Skip to content

Commit 0caca69

Browse files
wellweiwellweiSunrisepeakZheFeng7110
authored
fix: make the build.mcpp host helper self-contained on musl and MinGW (#298)
* fix: statically link musl build helpers * ci: use self-built mcpp in ARM source builds * fix: make the build.mcpp host helper self-contained on musl AND MinGW (2026.7.28.2) The compiled build.mcpp helper is exec'd by the host OS, outside anything mcpp controls — no wrapper, no injected LD_LIBRARY_PATH, no PATH guarantee. Whether it is actually runnable is a per-platform mechanism, and only two of the four were closed: Linux + glibc payload ok — host_base_flags bakes absolute payload paths into -Wl,-rpath Linux + musl (#295) bad — rpath cannot reach PT_INTERP, an absolute /lib/ld-musl-<arch>.so.1 no payload installs and no glibc distro ships -> execve ENOENT, surfacing as a bare exit 127 Windows PE (#299) bad — PE has no rpath at all, so the helper resolves libstdc++-6 / libgcc_s / libwinpthread via the process PATH -> STATUS_DLL_NOT_FOUND macOS ok — the system libc++ is always present Both gaps are the same defect (build_program.cppm re-derives the helper link policy instead of reusing the main build's, cf. prepare.cppm's musl -> linkage="static" default), so they get ONE decision point rather than two parallel predicates: a single staticHostHelper in run_build_program, with the chosen policy folded into the build-program cache identity so pre-fix helpers are rebuilt instead of reused. `-static` is appended to the final link argv only — never to `base`, which also feeds the bundled module's compile and precompile commands. Also: - process.cppm: keep the posix_spawnp error code and message in the captured output instead of collapsing to an unexplained exit 127 (both capture_exec and capture_exec_deadline). All three consumers of RunResult::output gate on exit_code first, so nothing downstream changes. - host_base_flags: stop emitting -Wl,-rpath on PE, where it is inert. - tests/e2e/168 (was 167 — taken on main by 167_build_defines_module_scan): discovers the installed musl-gcc payload instead of hardcoding 15.1.0, so it runs on the aarch64 host where the regression actually lives; asserts static + no PT_INTERP. Verified by negative control: reverting the -static push reproduces the exact #295 failure. - tests/e2e/97: hold the build.mcpp helper to the same standalone bar as the produced exe (import table + run with the toolchain bin off PATH). This runs under the real MinGW GCC 16.1.0 on Windows CI, which is #299's configuration. - ci-aarch64-fresh-install: append a PR regression gate that builds this source and runs e2e 168 against it. The checkout must come AFTER the fresh-install steps: .xlings.json declares an `mcpp` workspace pin, so a checkout present in $GITHUB_WORKSPACE makes `xlings install mcpp` workspace-scoped — which both voids the fresh-install charter (validating the pinned version, not the published one) and leaves bare `mcpp` unresolvable outside the workspace. Dropped the duplicated inline assertions, including `! readelf … | grep -q …`, which bash exempts from errexit and which could therefore never fail. Closes #295 Closes #299 Supersedes #300 Co-authored-by: Zhe Feng <zhefeng7110@outlook.com> --------- Co-authored-by: wellwei <1827104243@qq.com> Co-authored-by: sunrisepeak <speakshen@163.com> Co-authored-by: Zhe Feng <zhefeng7110@outlook.com>
1 parent a0d3ed1 commit 0caca69

8 files changed

Lines changed: 255 additions & 13 deletions

File tree

.github/workflows/ci-aarch64-fresh-install.yml

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,21 @@ on:
1616
workflow_dispatch:
1717
schedule:
1818
- cron: '0 6 * * 1' # weekly Mon 06:00 UTC
19+
pull_request:
20+
branches: [ main ]
21+
paths:
22+
- 'src/build/build_program.cppm'
23+
- 'src/platform/process.cppm'
24+
- 'tests/e2e/168_build_mcpp_musl_host_static.sh'
25+
- '.github/workflows/ci-aarch64-fresh-install.yml'
1926
push:
2027
branches: [ main ]
2128
paths:
2229
- '.github/workflows/ci-aarch64-fresh-install.yml'
2330

31+
permissions:
32+
contents: read
33+
2434
concurrency:
2535
group: ci-${{ github.workflow }}-${{ github.ref }}
2636
cancel-in-progress: true
@@ -34,6 +44,8 @@ jobs:
3444
# Verbose every mcpp invocation — cold bootstrap path (src/cli.cppm).
3545
MCPP_VERBOSE: "1"
3646
steps:
47+
# NB: the checkout deliberately comes LAST, after every fresh-install
48+
# step below — see the comment above it.
3749
- name: System info
3850
run: |
3951
uname -a
@@ -79,7 +91,7 @@ jobs:
7991
mcpp build
8092
out=$(mcpp run 2>/dev/null || true)
8193
echo "program output: $out"
82-
bin=$(find target -type f -name hello | head -1)
94+
bin=$(find target -type f -path '*/bin/hello' | head -1)
8395
file "$bin"
8496
file "$bin" | grep -q "ARM aarch64" || { echo "expected aarch64 ELF"; exit 1; }
8597
@@ -91,12 +103,39 @@ jobs:
91103
cd /tmp/mcpp-src
92104
mcpp self config --mirror GLOBAL 2>/dev/null || true
93105
mcpp build --target aarch64-linux-musl
94-
m=$(find target/aarch64-linux-musl -type f -name mcpp | head -1)
106+
m=$(find target/aarch64-linux-musl -type f -path '*/bin/mcpp' | head -1)
95107
file "$m" | grep -q "ARM aarch64" || { echo "expected aarch64 mcpp"; exit 1; }
96108
"$m" --version
97109
git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src
98110
cd /tmp/xlings-src
99111
mcpp build --target aarch64-linux-musl
100-
x=$(find target/aarch64-linux-musl -type f -name xlings | head -1)
112+
x=$(find target/aarch64-linux-musl -type f -path '*/bin/xlings' | head -1)
101113
file "$x" | grep -q "ARM aarch64" || { echo "expected aarch64 xlings"; exit 1; }
102114
"$x" --version
115+
116+
# ── PR regression gate ────────────────────────────────────────────────
117+
# Everything above is the fresh-install charter: it must run exactly as a
118+
# new user's machine does. Check out only NOW — this repo's .xlings.json
119+
# declares an `mcpp` WORKSPACE pin, so with the checkout present in
120+
# $GITHUB_WORKSPACE `xlings install mcpp` installs workspace-scoped
121+
# instead of globally: the steps above would silently validate the pinned
122+
# version rather than the freshly published one, and bare `mcpp` stops
123+
# resolving anywhere outside the workspace.
124+
- uses: actions/checkout@v4
125+
with:
126+
persist-credentials: false
127+
128+
# The PR's own source, then the musl host-helper regression against it.
129+
# This is the only runner where a musl toolchain is the NATIVE one, so it
130+
# is the only place #295 can actually be reproduced.
131+
- name: Build current mcpp source for native regression tests
132+
run: |
133+
mcpp build --target aarch64-linux-musl
134+
self=$(find target/aarch64-linux-musl -type f -path '*/bin/mcpp' | head -1)
135+
test -x "$self"
136+
self=$(realpath "$self")
137+
"$self" --version
138+
echo "MCPP_SELF=$self" >> "$GITHUB_ENV"
139+
140+
- name: "Regression: build.mcpp host helper is self-contained (#295)"
141+
run: MCPP="$MCPP_SELF" bash tests/e2e/168_build_mcpp_musl_host_static.sh

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.7.28.1"
3+
version = "2026.7.28.2"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/build_program.cppm

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.platform;
1818
import mcpp.platform.process;
1919
import mcpp.toolchain.fingerprint; // hash_file / hash_string (FNV-1a, 16 hex)
2020
import mcpp.toolchain.linkmodel; // shared C-library / clang-cfg-bypass model
21-
import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target
21+
import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target/is_mingw_target
2222
import mcpp.toolchain.registry; // archive_tool
2323
import mcpp.toolchain.triple; // host_triple (MCPP_HOST contract value)
2424
import mcpp.ui;
@@ -160,8 +160,10 @@ std::string env_value(const std::string& name) {
160160

161161
// The host subset of flags.cppm's sysroot/runtime handling — enough to compile +
162162
// link a freestanding host program on a fresh sandbox (where bare `g++ file -o x`
163-
// can't find crt/libc). build.mcpp is host-only (skipped under cross), so we need
164-
// only the native cases; these are passed as separate argv tokens (no shell).
163+
// can't find crt/libc). `tc` is always a HOST-targeting toolchain: under a cross
164+
// --target, prepare.cppm resolves the spec a second time without the target axis
165+
// (host_tc_for_build_program) and passes that here, so the native cases are the
166+
// only ones needed. Passed as separate argv tokens (no shell).
165167
std::vector<std::string> host_base_flags(const mcpp::toolchain::Toolchain& tc) {
166168
std::vector<std::string> f;
167169
const auto lm = mcpp::toolchain::resolve_link_model(tc);
@@ -228,9 +230,14 @@ std::vector<std::string> host_base_flags(const mcpp::toolchain::Toolchain& tc) {
228230
if (!ar.empty()) f.push_back("-B" + ar.parent_path().string());
229231
}
230232
// Runtime lib dirs so the produced program can load private libs in-tree.
233+
// -L is link-time and wanted everywhere; rpath is an ELF-only concept —
234+
// this is the one host_base_flags branch a PE target reaches, where the
235+
// flag is inert and the self-containment answer is the static link in
236+
// run_build_program instead (#299).
231237
for (auto& d : tc.linkRuntimeDirs) {
232238
f.push_back("-L" + d.string());
233-
f.push_back("-Wl,-rpath," + d.string());
239+
if constexpr (mcpp::platform::supports_rpath)
240+
f.push_back("-Wl,-rpath," + d.string());
234241
}
235242
return f;
236243
}
@@ -585,8 +592,37 @@ std::expected<void, std::string> run_build_program(
585592
auto childEnv = contract_env(root, outDir, env);
586593
std::string ctxHash = contract_hash(childEnv);
587594

595+
// ── Helper self-containment (the single decision point) ─────────────────
596+
// The compiled helper is exec'd by the host OS, outside anything mcpp
597+
// controls — no wrapper, no injected LD_LIBRARY_PATH, no PATH guarantee.
598+
// Making it runnable is a per-platform mechanism, and only two of the four
599+
// need a static link:
600+
// Linux + glibc payload — host_base_flags bakes absolute payload paths
601+
// into -Wl,-rpath; that already closes it, so leave it dynamic.
602+
// Linux + musl (#295) — rpath cannot reach PT_INTERP, an absolute
603+
// /lib/ld-musl-<arch>.so.1 that no payload installs and glibc distros
604+
// do not ship. Only a static link removes the interpreter entirely.
605+
// Windows PE (#299) — PE has no rpath, so a dynamic helper resolves
606+
// libstdc++-6 / libgcc_s / libwinpthread through the process PATH and
607+
// dies with STATUS_DLL_NOT_FOUND unless the user put the toolchain bin
608+
// there by hand. A static link is the only PATH-independent answer.
609+
// macOS — the system libc++ is always present.
610+
// Keep this the ONLY place that decides; the flag is appended to the final
611+
// link argv further down, never to `base`.
612+
const bool muslStaticHelper = mcpp::platform::supports_full_static
613+
&& mcpp::toolchain::is_musl_target(tc);
614+
const bool mingwStaticHelper = mcpp::toolchain::is_mingw_target(tc);
615+
const bool staticHostHelper = muslStaticHelper || mingwStaticHelper;
616+
617+
// Fold the policy into the compiler identity: a helper produced under an
618+
// older link policy must be rebuilt, not reused from the cache.
619+
std::string compilerIdentity = hostCompiler.string();
620+
compilerIdentity += "\nbuild-program-link=";
621+
compilerIdentity += muslStaticHelper ? "musl-static-v1"
622+
: mingwStaticHelper ? "mingw-static-v1"
623+
: "default-v1";
588624
std::string programHash = mcpp::toolchain::hash_file(src);
589-
std::string compilerHash = mcpp::toolchain::hash_string(hostCompiler.string());
625+
std::string compilerHash = mcpp::toolchain::hash_string(compilerIdentity);
590626

591627
// Fast path: declared inputs + contract unchanged → reapply cached
592628
// directives, no run.
@@ -641,6 +677,11 @@ std::expected<void, std::string> run_build_program(
641677
compileArgv.push_back("-x"); compileArgv.push_back("none");
642678
compileArgv.push_back((bdir / "mcpp.o").string());
643679
}
680+
// Self-contained helper link — see the staticHostHelper doctrine above.
681+
// Deliberately NOT in `base`: that also feeds the bundled module's
682+
// compile/precompile commands, where a link flag has no business (and for
683+
// Clang would perturb the default PIC/PIE codegen of mcpp.o).
684+
if (staticHostHelper) compileArgv.push_back("-static");
644685
compileArgv.push_back("-o"); compileArgv.push_back(bin.string());
645686
mcpp::ui::info("build.mcpp", "compiling");
646687
// GCC resolves `import mcpp;` via gcm.cache/ relative to the compile cwd, so

src/platform/process.cppm

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ std::vector<std::string> merged_environ(
230230
}
231231
return out;
232232
}
233+
234+
std::string spawn_failure(std::string_view program, int error) {
235+
return std::format("posix_spawnp('{}') failed (error {}): {}\n",
236+
program, error, std::generic_category().message(error));
237+
}
233238
#else
234239
// Build a shell command line from an argv vector (Windows + residual non-POSIX
235240
// fallback only; Linux/macOS exec directly, #248). The first token (program)
@@ -444,7 +449,12 @@ RunResult capture_exec(
444449
int sp = ::posix_spawnp(&pid, cargv[0], &fa, nullptr, cargv.data(), envp.data());
445450
::posix_spawn_file_actions_destroy(&fa);
446451
::close(fds[1]);
447-
if (sp != 0) { ::close(fds[0]); result.exit_code = 127; return result; }
452+
if (sp != 0) {
453+
::close(fds[0]);
454+
result.exit_code = 127;
455+
result.output = spawn_failure(argv.front(), sp);
456+
return result;
457+
}
448458

449459
std::array<char, 4096> buf{};
450460
ssize_t n;
@@ -547,7 +557,12 @@ RunResult capture_exec_deadline(
547557
int sp = ::posix_spawnp(&pid, cargv[0], &fa, nullptr, cargv.data(), envp.data());
548558
::posix_spawn_file_actions_destroy(&fa);
549559
::close(fds[1]);
550-
if (sp != 0) { ::close(fds[0]); result.exit_code = 127; return result; }
560+
if (sp != 0) {
561+
::close(fds[0]);
562+
result.exit_code = 127;
563+
result.output = spawn_failure(argv.front(), sp);
564+
return result;
565+
}
551566

552567
::fcntl(fds[0], F_SETFL, ::fcntl(fds[0], F_GETFL) | O_NONBLOCK);
553568

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "2026.7.28.1";
21+
inline constexpr std::string_view MCPP_VERSION = "2026.7.28.2";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env bash
2+
# requires: elf
3+
# 168_build_mcpp_musl_host_static.sh — a build.mcpp is a HOST executable that
4+
# the OS execs directly. Under a musl host toolchain it must be fully static:
5+
# a dynamic musl binary carries PT_INTERP=/lib/ld-musl-<arch>.so.1, an absolute
6+
# path no toolchain payload installs and no glibc distro ships, so execve fails
7+
# with ENOENT and the build dies as an unexplained exit 127 (#295).
8+
#
9+
# Runs standalone too — the aarch64 fresh-install workflow invokes it directly:
10+
# MCPP=/path/to/mcpp bash tests/e2e/168_build_mcpp_musl_host_static.sh
11+
set -euo pipefail
12+
13+
if [[ "$(uname -s)" != "Linux" ]]; then
14+
echo "SKIP: Linux ELF regression"
15+
echo "OK"
16+
exit 0
17+
fi
18+
19+
: "${MCPP:?MCPP must point to the mcpp binary under test}"
20+
21+
# Which musl toolchain to pin is a property of the machine, not of the test:
22+
# x86_64 dev boxes carry 15.1.0, the aarch64 runner resolves 16.1.0. Discover
23+
# the installed payload instead of hardcoding either — which is also why this
24+
# owns its own precondition (`requires: elf`, not `requires: musl`): run_all.sh's
25+
# musl capability probe is pinned to x86_64/15.1.0 and would false-skip on the
26+
# aarch64 host where this regression actually lives. Never auto-install here:
27+
# that would be a ~200 MB download per CI run (same rule as 28_target_static).
28+
find_musl_version() {
29+
local root gxx
30+
for root in "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-musl-gcc" \
31+
"$HOME/.xlings/data/xpkgs/xim-x-musl-gcc"; do
32+
[[ -d "$root" ]] || continue
33+
gxx=$(ls "$root"/*/bin/*-linux-musl-g++ 2>/dev/null | head -1 || true)
34+
if [[ -n "$gxx" ]]; then
35+
# <root>/<version>/bin/<triple>-g++ → <version>
36+
basename "$(dirname "$(dirname "$gxx")")"
37+
return 0
38+
fi
39+
done
40+
return 1
41+
}
42+
43+
MUSL_VERSION=$(find_musl_version || true)
44+
if [[ -z "$MUSL_VERSION" ]]; then
45+
echo "SKIP: no musl-gcc payload installed — not testing the musl host helper"
46+
echo "OK"
47+
exit 0
48+
fi
49+
echo "musl toolchain: gcc@${MUSL_VERSION}-musl"
50+
51+
TMP=$(mktemp -d)
52+
trap 'rm -rf "$TMP"' EXIT
53+
cd "$TMP"
54+
"$MCPP" new app > new.log 2>&1 || {
55+
cat new.log
56+
echo "FAIL: mcpp new failed"
57+
exit 1
58+
}
59+
cd app
60+
61+
cat >> mcpp.toml <<EOF
62+
63+
[toolchain]
64+
default = "gcc@${MUSL_VERSION}-musl"
65+
EOF
66+
67+
cat > build.mcpp <<'EOF'
68+
#include <cstdio>
69+
int main() {
70+
FILE* marker = std::fopen("helper-ran", "w");
71+
if (!marker) return 2;
72+
std::fputs("ok\n", marker);
73+
std::fclose(marker);
74+
std::puts("mcpp:rerun-if-changed=build.mcpp");
75+
return 0;
76+
}
77+
EOF
78+
79+
"$MCPP" build > build.log 2>&1 || {
80+
cat build.log
81+
echo "FAIL: musl build.mcpp helper did not run on this host"
82+
exit 1
83+
}
84+
85+
helper=target/.build-mcpp/build.mcpp.bin
86+
test -f helper-ran || { cat build.log; echo "FAIL: build.mcpp did not run"; exit 1; }
87+
test -x "$helper" || { cat build.log; echo "FAIL: helper binary missing"; exit 1; }
88+
89+
file "$helper"
90+
file "$helper" | grep -q 'statically linked' || {
91+
echo "FAIL: musl host helper is not statically linked"
92+
exit 1
93+
}
94+
# The property that actually decides whether execve succeeds. NB: an `if`, not
95+
# `! readelf ... | grep -q ...` — bash exempts !-inverted pipelines from
96+
# errexit, so the negated form would never fail the test.
97+
if readelf -lW "$helper" | grep -q 'Requesting program interpreter'; then
98+
readelf -lW "$helper"
99+
echo "FAIL: musl host helper contains PT_INTERP"
100+
exit 1
101+
fi
102+
103+
echo "OK"

tests/e2e/97_mingw_toolchain.sh

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# - new → build → run works with import std + a named module (gcm pipeline)
88
# - the produced exe is portable: runs from a clean dir without the
99
# toolchain's bin on PATH (static libstdc++/libgcc defaults)
10+
# - so is the build.mcpp HOST helper mcpp execs mid-build (#299)
1011
set -e
1112

1213
CONF="${MCPP_HOME:-$HOME/.mcpp}/config.toml"
@@ -87,4 +88,39 @@ if [[ $iso_rc -ne 0 || "$iso_out" != *"mingw-ok"* ]]; then
8788
exit 1
8889
fi
8990

90-
echo "PASS: mingw toolchain — install, default, modules build/run, standalone exe"
91+
# 5) the build.mcpp HOST helper is held to the same bar (#299). It is exec'd
92+
# by mcpp mid-build, and PE has no rpath — a dynamic helper resolves
93+
# libstdc++-6 / libgcc_s / libwinpthread through the process PATH and dies
94+
# with STATUS_DLL_NOT_FOUND (exit -1073741511) unless the user added the
95+
# toolchain bin to PATH by hand.
96+
cat > build.mcpp <<'EOF'
97+
#include <cstdio>
98+
int main() {
99+
FILE* marker = std::fopen("helper-ran", "w");
100+
if (!marker) return 2;
101+
std::fputs("ok\n", marker);
102+
std::fclose(marker);
103+
std::puts("mcpp:rerun-if-changed=build.mcpp");
104+
return 0;
105+
}
106+
EOF
107+
out=$("$MCPP" build 2>&1) || { echo "FAIL: build with build.mcpp: $out"; exit 1; }
108+
[[ -f helper-ran ]] || { echo "FAIL: build.mcpp did not run: $out"; exit 1; }
109+
HELPER="target/.build-mcpp/build.mcpp.exe"
110+
[[ -f "$HELPER" ]] || { echo "FAIL: no helper binary at $HELPER"; exit 1; }
111+
if [[ -x "$OBJDUMP" ]]; then
112+
h_imports=$("$OBJDUMP" -p "$HELPER" 2>/dev/null | grep -i "DLL Name" || true)
113+
echo "helper imports:"; echo "$h_imports"
114+
bad=$(echo "$h_imports" | grep -iE "libstdc|libgcc|libwinpthread" || true)
115+
[[ -z "$bad" ]] || { echo "FAIL: toolchain DLLs in helper import table: $bad"; exit 1; }
116+
fi
117+
HISO="$TMP/hiso"; mkdir -p "$HISO"
118+
cp "$HELPER" "$HISO/"
119+
h_rc=0
120+
(cd "$HISO" && PATH="/usr/bin:/c/Windows/System32" ./build.mcpp.exe >/dev/null 2>&1) || h_rc=$?
121+
[[ $h_rc -eq 0 ]] || {
122+
echo "FAIL: build.mcpp helper does not run without the toolchain bin on PATH (rc=$h_rc)"
123+
exit 1
124+
}
125+
126+
echo "PASS: mingw toolchain — install, default, modules build/run, standalone exe + build.mcpp helper"

tests/unit/test_process_run_exec.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ TEST(RunExec, ReturnsErrorWhenProgramMissing) {
4444
EXPECT_NE(process::run_exec({"/no/such/program/mcpp-xyz"}), 0);
4545
}
4646

47+
TEST(CaptureExec, MissingProgramReportsSpawnFailure) {
48+
constexpr std::string_view missing = "/no/such/program/mcpp-capture-xyz";
49+
auto r = process::capture_exec({std::string(missing)});
50+
EXPECT_EQ(r.exit_code, 127);
51+
EXPECT_NE(r.output.find(missing), std::string::npos) << r.output;
52+
EXPECT_NE(r.output.find("error 2"), std::string::npos) << r.output;
53+
}
54+
4755
TEST(CaptureExec, CapturesStdoutWithoutShell) {
4856
auto r = process::capture_exec({"/bin/echo", "hello world"});
4957
EXPECT_EQ(r.exit_code, 0);

0 commit comments

Comments
 (0)