fix: decide a build step by its exit code, not by whether it printed

RunCommand merged stderr into stdout, dropped pclose's status and returned
the text; every compile, link and archive site then read "printed something"
as "failed". A warning is printing something, so a translation unit that
warned failed the build — but only on the run that actually recompiled it,
since warnings aren't re-emitted for an object that's already up to date.
The same unchanged source therefore passed or failed depending on the state
of the build tree: flaky-looking tests locally, and a cold CI checkout
surfacing every latent warning in a project at once as unrelated failures.

RunCommand is gone, replaced by RunBuildCommand: it goes through
RunCommandChecked, returns "" when the command exited 0 (so every caller's
`if (!result.empty())` error path is unchanged) and hands any warnings to
the new Progress::Diagnostic instead of to the error path. That also closes
the quiet half of the bug — a compiler killed by the OOM killer prints
nothing, so it used to read as success and leave the build carrying on with
a missing object; it now reports the signal that killed it.

Warnings are now shown rather than swallowed, which they weren't in either
direction before: invisible on an incremental build, fatal on a cold one.
Failing on them stays a project's choice, via -Werror in compileFlags.
This commit is contained in:
catbot 2026-08-26 00:51:15 +00:00
commit 8e2d21a2bc
7 changed files with 77 additions and 64 deletions

View file

@ -259,28 +259,34 @@ bool Crafter::MatchAny(std::span<const std::string> globs, std::string_view name
return false;
}
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
std::string Crafter::RunCommand(const std::string_view cmd) {
// See the declaration in Crafter.Build-Platform.cppm for why this branches on
// the exit code rather than on whether the command said anything.
std::string Crafter::RunBuildCommand(std::string_view cmd) {
Progress::EchoCommand(cmd);
std::array<char, 128> buffer;
std::string result;
CommandResult r = RunCommandChecked(cmd);
// Use cmd.exe to interpret redirection
std::string with = std::format("cmd /C \"{} 2>&1\"", std::string(cmd));
FILE* pipe = _popen(with.c_str(), "r");
if (!pipe) {
throw std::runtime_error("_popen() failed!");
if (r.exitCode == 0) {
// Warnings from a step that succeeded. Show them — until now they were
// invisible on an incremental build and fatal on a cold one.
if (!r.output.empty()) Progress::Diagnostic(r.output);
return "";
}
while (fgets(buffer.data(), static_cast<std::int32_t>(buffer.size()), pipe) != nullptr) {
result += buffer.data();
// A compiler killed outright (the OOM killer is the common one) prints
// nothing, so the old "output means failure" rule read it as success and
// let the build carry on with a missing object. Say what happened instead.
if (r.output.empty()) {
return r.crashed
? std::format("terminated by signal {}: {}", r.signal, cmd)
: std::format("exited with code {}: {}", r.exitCode, cmd);
}
_pclose(pipe);
return result;
if (r.crashed) {
return std::format("{}\nterminated by signal {}", r.output, r.signal);
}
return r.output;
}
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{};
@ -429,7 +435,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
CacheLock lock(stdPcm.parent_path());
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdcppm)) {
return RunCommand(std::format("clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 -nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}", config.target, config.march, config.mtune, stdPcm.string()));
return RunBuildCommand(std::format("clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 -nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}", config.target, config.march, config.mtune, stdPcm.string()));
}
return "";
}
@ -521,7 +527,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
cacheDir.string(),
absProject.string(), crafterBuildLib.string(), dllPath.string());
std::string result = RunCommand(compileCmd);
std::string result = RunBuildCommand(compileCmd);
if (!result.empty()) {
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
}
@ -597,7 +603,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
if (fs::exists(stdPcm) && fs::last_write_time(stdPcm) >= fs::last_write_time(stdcppm)) {
return "";
}
return RunCommand(std::format(
return RunBuildCommand(std::format(
"clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
"-nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier "
"--precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}",
@ -613,17 +619,17 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
if (fs::exists(stdPcm) && fs::last_write_time(stdPcm) >= fs::last_write_time(stdCc)) {
return "";
}
// Copy std.cc → std.cppm in C++ rather than via cmd's `copy /Y` because
// `copy` always prints "1 file(s) copied." to stdout and RunCommand
// treats any output as an error. Held under the cache lock, so a plain
// filename is safe — no other builder writes this path concurrently.
// Copy std.cc → std.cppm in C++ rather than via cmd's `copy /Y`: keeping
// it in-process means one less shell round-trip and a std::error_code we
// can report directly. Held under the cache lock, so a plain filename is
// safe — no other builder writes this path concurrently.
fs::path stdCppm = stdPcm.parent_path() / "std.cppm";
std::error_code ec;
fs::copy_file(stdCc, stdCppm, fs::copy_options::overwrite_existing, ec);
if (ec) {
return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), ec.message());
}
return RunCommand(std::format(
return RunBuildCommand(std::format(
"clang++ --target={} -march={} -mtune={} "
"--sysroot=\"{}\" -femulated-tls "
"-O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier "
@ -730,7 +736,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
absProject.string(), dllPath.string(),
hostExe.parent_path().string());
std::string result = RunCommand(compileCmd);
std::string result = RunBuildCommand(compileCmd);
if (!result.empty()) {
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
}
@ -761,26 +767,6 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
std::string Crafter::RunCommand(const std::string_view cmd) {
Progress::EchoCommand(cmd);
std::array<char, 128> buffer;
std::string result;
std::string with = std::format("{} 2>&1", cmd);
// Open pipe to file
FILE* pipe = popen(with.c_str(), "r");
if (!pipe) throw std::runtime_error("popen() failed!");
// Read till end of process:
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
result += buffer.data();
}
// Close pipe
pclose(pipe);
return result;
}
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{};
@ -874,7 +860,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
if (copyEc) {
return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), copyEc.message());
}
return RunCommand(std::format("clang++ --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, config.march, config.mtune, stdCppm.string(), stdPcm.string()));
return RunBuildCommand(std::format("clang++ --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, config.march, config.mtune, stdCppm.string(), stdPcm.string()));
} else {
return "";
}
@ -922,7 +908,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
}
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
return RunBuildCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
} else {
return "";
}
@ -1038,7 +1024,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
cacheDir.string(),
absProject.string(), soPath.string());
std::string result = RunCommand(compileCmd);
std::string result = RunBuildCommand(compileCmd);
if (!result.empty()) {
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
}