Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
705 changes: 705 additions & 0 deletions .agents/docs/2026-06-19-runtime-launch-and-multi-distro-plan.md

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions .github/workflows/ci-fresh-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,83 @@ jobs:
mcpp clean
mcpp run

# ──────────────────────────────────────────────────────────────────
# Newer/rolling-glibc distros — reproduction surface for the
# bundled-glibc-vs-host-libtinfo `sh:` crash (host glibc > bundled).
# Plus older-glibc legs (the safe reverse direction) proving the
# musl-static mcpp + self-contained toolchain run end-to-end on old
# hosts. Runs the released mcpp inside distro containers.
# ──────────────────────────────────────────────────────────────────
linux-distro-matrix:
name: Linux distro (${{ matrix.distro }})
runs-on: ubuntu-24.04
container:
image: ${{ matrix.image }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- distro: fedora-latest
image: fedora:latest
setup: dnf -y install curl bash tar gzip xz git findutils binutils file glibc-langpack-en
- distro: arch
image: archlinux:latest
setup: pacman -Sy --noconfirm curl bash tar gzip xz git binutils file
- distro: tumbleweed
image: opensuse/tumbleweed:latest
setup: zypper -n install curl bash tar gzip xz git binutils file
- distro: debian-testing
image: debian:testing
setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file
- distro: ubuntu-2004
image: ubuntu:20.04
setup: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file
- distro: debian-11
image: debian:11
setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file
env:
XLINGS_NON_INTERACTIVE: '1'
HOME: /root
steps:
- uses: actions/checkout@v4

- name: Install prerequisites (${{ matrix.distro }})
run: ${{ matrix.setup }}

- name: Install xlings + mcpp
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.38
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Configure mcpp
run: |
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
xlings update
xlings install mcpp -y -g
mcpp --version
mcpp self config --mirror GLOBAL

- name: "Regression: new → run (loader env must not crash /bin/sh)"
run: |
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
cd "$(mktemp -d)"
mcpp new hello_distro
cd hello_distro
mcpp run

- name: "Self-containment: produced binary uses bundled loader"
run: |
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
cd "$(mktemp -d)" && mcpp new hc && cd hc && mcpp build
bin="$(find target -type f -name hc | head -1)"
interp="$(file "$bin" | grep -o 'interpreter [^,]*' | awk '{print $2}')"
echo "interp=$interp"
case "$interp" in
*/.mcpp/*|*/registry/*|*xpkgs*) echo "OK bundled loader" ;;
*) echo "FAIL host loader: $interp"; exit 1 ;;
esac

# ──────────────────────────────────────────────────────────────────
# macOS: llvm@20.1.7
# ──────────────────────────────────────────────────────────────────
Expand Down
78 changes: 44 additions & 34 deletions src/build/execute.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ export std::optional<int> try_fast_build(const std::filesystem::path& projectRoo

auto outputDirStr = match->outputDir;
auto ninjaProgram = match->ninjaProgram;
// Legacy caches stored a shell-quoted path; execvp needs the raw path.
if (ninjaProgram.size() >= 2 && ninjaProgram.front() == '\''
&& ninjaProgram.back() == '\'')
ninjaProgram = ninjaProgram.substr(1, ninjaProgram.size() - 2);
auto cachedFingerprint = match->fingerprint;
auto runtimeEnvKey = match->runtimeEnvKey;
auto runtimeEnvValue = match->runtimeEnvValue;
Expand Down Expand Up @@ -317,19 +321,21 @@ export std::optional<int> try_fast_build(const std::filesystem::path& projectRoo
}

// All inputs are older than build.ninja → fast-path: just run ninja.
std::string cmd = ninjaProgram;
if (!verbose) cmd += " --quiet";
cmd += std::format(" -C {}", mcpp::platform::shell::quote(outputDir.string()));
if (verbose) cmd += " -v";
cmd += " 2>&1";
std::vector<std::string> argv{ninjaProgram};
if (!verbose) argv.push_back("--quiet");
argv.push_back("-C");
argv.push_back(outputDir.string());
if (verbose) argv.push_back("-v");

auto t0 = std::chrono::steady_clock::now();
std::string out;
std::optional<mcpp::platform::env::ScopedEnv> scopedEnv;
std::vector<std::pair<std::string, std::string>> childEnv;
if (runtimeEnvKey != "-" && !runtimeEnvValue.empty())
scopedEnv.emplace(runtimeEnvKey, runtimeEnvValue);
auto r = mcpp::platform::process::capture(cmd);
out = r.output;
childEnv.emplace_back(runtimeEnvKey, runtimeEnvValue);

auto t0 = std::chrono::steady_clock::now();
// capture_exec merges stderr into the captured output (replacing `2>&1`),
// so is_stale_ninja_failure / filter_ninja_output still see ninja errors.
auto r = mcpp::platform::process::capture_exec(argv, childEnv);
std::string out = r.output;
int status = r.exit_code;
bool ok = (status == 0);
if (!ok) {
Expand Down Expand Up @@ -386,19 +392,21 @@ export int build_run_target(const std::optional<std::string>& targetName,
std::format("`{}`", mcpp::ui::shorten_path(exe, pathCtx)));
std::println("");
std::fflush(stdout);
std::string cmd = mcpp::platform::shell::quote(exe.string());
for (auto& a : passthrough) cmd += " " + mcpp::platform::shell::quote(a);
std::vector<std::string> argv;
argv.push_back(exe.string());
for (auto& a : passthrough) argv.push_back(a);

std::optional<mcpp::platform::env::ScopedEnv> runtimeEnv;
std::vector<std::pair<std::string, std::string>> childEnv;
auto runtimeEnvKey = mcpp::platform::env::runtime_library_path_key();
auto runtimeEnvValue = mcpp::platform::env::prepend_path_list(
runtimeEnvKey, ctx->plan.runtimeLibraryDirs);
if (!runtimeEnvKey.empty() && !runtimeEnvValue.empty()) {
runtimeEnv.emplace(runtimeEnvKey, runtimeEnvValue);
}
if (!runtimeEnvKey.empty() && !runtimeEnvValue.empty())
childEnv.emplace_back(runtimeEnvKey, runtimeEnvValue);

int rc = std::system(cmd.c_str());
return mcpp::platform::process::extract_exit_code(rc) == 0 ? 0 : 1;
// Direct exec (no /bin/sh): the loader env reaches ONLY the target child,
// never mcpp or a host shell. Fixes the bundled-glibc-vs-host-libtinfo
// crash on newer-glibc distros.
return mcpp::platform::process::run_exec(argv, childEnv) == 0 ? 0 : 1;
}

// `mcpp test` driver: discover tests/**/*.cpp, synthesize targets, build
Expand Down Expand Up @@ -505,38 +513,40 @@ export int run_tests(std::span<const std::string> passthrough,
int failed = 0;
std::vector<std::string> failures;

std::optional<mcpp::platform::env::ScopedEnv> runtimeEnv;
auto runtimeEnvKey = mcpp::platform::env::runtime_library_path_key();
auto runtimeEnvValue = mcpp::platform::env::prepend_path_list(
runtimeEnvKey, ctx->plan.runtimeLibraryDirs);
if (!runtimeEnvKey.empty() && !runtimeEnvValue.empty()) {
runtimeEnv.emplace(runtimeEnvKey, runtimeEnvValue);
}

for (auto& lu : ctx->plan.linkUnits) {
if (lu.kind != mcpp::build::LinkUnit::TestBinary) continue;
auto exe = ctx->outputDir / lu.output;
mcpp::ui::status("Running", std::format("bin/{}", lu.targetName));

// Prepend the sandbox's subos/default/bin to PATH so tools
// bootstrapped during sandbox init (patchelf, ninja, etc.) are
// visible to test binaries that shell out to them. The
// toolchain binary's path encodes the registry root — derive it.
std::string pathPrefix;
std::vector<std::string> argv;
argv.push_back(exe.string());
for (auto& a : passthrough) argv.push_back(a);

std::vector<std::pair<std::string, std::string>> childEnv;
if (!runtimeEnvKey.empty() && !runtimeEnvValue.empty())
childEnv.emplace_back(runtimeEnvKey, runtimeEnvValue);

// Prepend the sandbox's subos/default/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 (!mcpp::platform::is_windows) {
if (auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(ctx->tc.binaryPath)) {
// xpkgs is <registry>/data/xpkgs → registry = xpkgs/../..
auto registryDir = xpkgs->parent_path().parent_path();
auto sandboxBin = registryDir / "subos" / "default" / "bin";
if (std::filesystem::exists(sandboxBin))
pathPrefix = std::format("PATH={}:\"$PATH\" ",
mcpp::platform::shell::quote(sandboxBin.string()));
if (std::filesystem::exists(sandboxBin)) {
std::array<std::filesystem::path, 1> extra{sandboxBin};
auto pathVal = mcpp::platform::env::prepend_path_list("PATH", extra);
if (!pathVal.empty()) childEnv.emplace_back("PATH", pathVal);
}
}
}

std::string cmd = pathPrefix + mcpp::platform::shell::quote(exe.string());
for (auto& a : passthrough) cmd += " " + mcpp::platform::shell::quote(a);
int exitCode = mcpp::platform::process::extract_exit_code(std::system(cmd.c_str()));
int exitCode = mcpp::platform::process::run_exec(argv, childEnv);

if (exitCode == 0) {
std::println("{} ... ok", lu.targetName);
Expand Down
40 changes: 21 additions & 19 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -714,15 +714,11 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
ninjaBin = *nb;
}

std::string ninjaProgram;
if (!ninjaBin.empty()) {
if constexpr (mcpp::platform::is_windows)
ninjaProgram = ninjaBin.string();
else
ninjaProgram = mcpp::platform::shell::quote(ninjaBin.string());
} else {
ninjaProgram = "ninja";
}
// Raw program path (no shell quoting): recorded in the fast-path cache and
// exec'd directly via capture_exec/execvp, which take argv (not a shell
// string). Shell-using call sites must quote it locally.
std::string ninjaProgram = ninjaBin.empty() ? std::string("ninja")
: ninjaBin.string();

// Record ninja binary for P0 fast-path cache.
BuildResult r;
Expand All @@ -734,21 +730,27 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
r.runtimeEnvKey = "-";
}

std::string cmd = ninjaProgram;
// Direct exec (no /bin/sh): argv, not a shell string. capture_exec merges
// stderr into the captured output (replacing the old `2>&1`), and applies
// the runtime env to the child ONLY — so a bundled-glibc LD_LIBRARY_PATH
// can never poison the host shell (the newer-glibc `sh:` crash class).
std::vector<std::string> nargv{ninjaProgram};
if (!opts.verbose)
cmd += " --quiet";
cmd += std::format(" -C {}", mcpp::xlings::shq(plan.outputDir.string()));
nargv.push_back("--quiet");
nargv.push_back("-C");
nargv.push_back(plan.outputDir.string());
if (opts.verbose)
cmd += " -v";
nargv.push_back("-v");
if (opts.parallelJobs)
cmd += std::format(" -j{}", opts.parallelJobs);
cmd += " 2>&1";
nargv.push_back(std::format("-j{}", opts.parallelJobs));

std::string out;
std::optional<mcpp::platform::env::ScopedEnv> scopedEnv;
std::vector<std::pair<std::string, std::string>> nenv;
if (r.runtimeEnvKey != "-" && !r.runtimeEnvValue.empty())
scopedEnv.emplace(r.runtimeEnvKey, r.runtimeEnvValue);
bool ok = run(cmd, out, /*capture=*/true);
nenv.emplace_back(r.runtimeEnvKey, r.runtimeEnvValue);

auto cap = mcpp::platform::process::capture_exec(nargv, nenv);
std::string out = cap.output;
bool ok = (cap.exit_code == 0);

r.exitCode = ok ? 0 : 1;
r.elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
Expand Down
Loading
Loading