Repository: ninja-build/ninja (C++)

Issue:
Return codes to distinguish when ninja fails to execute the flow or to startup the execution
Currently, on linux, the ninja process exits with status 1 if any kind of error exists.

I propose two different exit statuses, for the ninja process, to determine whether:
1. the flow did not started the execution at all, i.e. ninja file has a syntax error
2. or the flow execution has failed 

The main goal is to simplify the implementation of automated workflows based on ninja files generators.

Adriano

Ninja hides the exit status of failing subprocesses
In build.cc, Ninja checks if the subprocess succeeded or failed

```
  // Print the command that is spewing before printing its output.
  if (!success) {
    string outputs;
    for (vector<Node*>::const_iterator o = edge->outputs_.begin();
         o != edge->outputs_.end(); ++o)
      outputs += (*o)->path() + " ";

    printer_.PrintOnNewLine("FAILED: " + outputs + "\n");
    printer_.PrintOnNewLine(edge->EvaluateCommand() + "\n");
  }
```

But by this point in the build, Ninja has already discarded the actual exit value from the subprocess.

There are a variety of tools that don't print anything to the console when they fail to run, relying on the user to instead check the exit status to see what the reason for failure was.

This is especially problematic for third party tools where the source code is not available.

It's, of course, possible to get the exit status printed to the console using a wrapper script, but as discussed at extreme length, wrapper scripts are desired by basically no one.

Ninja should print the exit status of failing subprocesses.


Discussion: Sounds like a good idea.


I don't see any "FAILED:" message, but instead
```ninja: build stopped: subcommand failed```

Is it the same issue? 

@schultetwin1 Would https://github.com/ninja-build/ninja/pull/1805 address this situation too, and reveal the exit code of the failed subcommand?
@jimis if you are not seeing any "FAILED:" message this seems like a different issue. #1805 only adds to the "FAILED:" string. #1805 has no changes that would cause that "FAILED:" string to appear when it currently does not.
Thanks @schultetwin1. Indeed it seems the "subcommand failed" message is coming from https://github.com/ninja-build/ninja/blob/54959b0f2c4950d97d94c03810b3b5185be0d69e/src/build.cc#L883 which is not touched in your patch.

The effect is the same however: an exit code that is lost, while it could have been printed.
@jonesmz @jimis @schultetwin1 have you guys figured this out?

My make file:
```
#!/bin/bash
# Build script
export CC=/usr/bin/clang
export CXX=/usr/bin/clang++
mkdir -p build
cd build
cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release
ninja
cd ..
```

Error in github actions (it still thinks the build is successful):
```
Run ./make
-- The C compiler identification is AppleClang 12.0.0.12000032
-- The CXX compiler identification is AppleClang 12.0.0.12000032
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode_12.2.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode_12.2.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Build spdlog: 1.8.2
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE  
-- Build type: Release
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/runner/work/pvphero-launcher/pvphero-launcher/build
[1/20] Copying OS X Content PvPHero.app/Contents/Resources/icon.icns
[2/20] Building CXX object deps/spdlog/CMakeFiles/spdlog.dir/src/file_sinks.cpp.o
[3/20] Building CXX object deps/spdlog/CMakeFiles/spdlog.dir/src/stdout_sinks.cpp.o
[4/20] Building CXX object deps/spdlog/CMakeFiles/spdlog.dir/src/color_sinks.cpp.o
[5/20] Building CXX object deps/spdlog/CMakeFiles/spdlog.dir/src/spdlog.cpp.o
[6/20] Building CXX object CMakeFiles/PvPHero.dir/src/updating/Downloader.cpp.o
[7/20] Building CXX object CMakeFiles/PvPHero.dir/src/updating/client/ClientUpdater.cpp.o
[8/20] Building CXX object CMakeFiles/PvPHero.dir/src/PvPHero.cpp.o
[9/20] Building CXX object CMakeFiles/PvPHero.dir/src/external/sago/platform_folders.cpp.o
[10/20] Building CXX object CMakeFiles/PvPHero.dir/src/filesystem/InMemoryFileSystem.cpp.o
[11/20] Building CXX object CMakeFiles/PvPHero.dir/src/updating/Utils.cpp.o
../src/updating/Utils.cpp:61:39: warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]
        const char *command_line[] = {getJavaPath().c_str(), "-m", "pvphero/com.pvphero.PvPHero",
                                      ^~~~~~~~~~~~~
1 warning generated.
[12/20] Building C object CMakeFiles/PvPHero.dir/src/external/zip/zip.c.o
[13/20] Building CXX object CMakeFiles/PvPHero.dir/src/Assets.cpp.o
../src/Assets.cpp:2:9: warning: #pragma once in main file [-Wpragma-once-outside-header]
#pragma once
        ^
1 warning generated.
[14/20] Building CXX object deps/spdlog/CMakeFiles/spdlog.dir/src/async.cpp.o
[15/20] Building CXX object deps/spdlog/CMakeFiles/spdlog.dir/src/cfg.cpp.o
[16/20] Building CXX object CMakeFiles/PvPHero.dir/src/updating/launcher/LauncherUpdater.cpp.o
[17/20] Building CXX object deps/spdlog/CMakeFiles/spdlog.dir/src/fmt.cpp.o
[18/20] Linking CXX static library deps/spdlog/libspdlog.a
[19/20] Building CXX object CMakeFiles/PvPHero.dir/src/main.cpp.o
[20/20] Linking CXX executable PvPHero.app/Contents/MacOS/PvPHero
FAILED: PvPHero.app/Contents/MacOS/PvPHero 
: && /Applications/Xcode_12.2.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++ -O3 -DNDEBUG -isysroot /Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk -mmacosx-version-min=10.15 -Wl,-search_paths_first -Wl,-headerpad_max_install_names /SUBSYSTEM:CONSOLE -s CMakeFiles/PvPHero.dir/src/PvPHero.cpp.o CMakeFiles/PvPHero.dir/src/updating/Utils.cpp.o CMakeFiles/PvPHero.dir/src/updating/Downloader.cpp.o CMakeFiles/PvPHero.dir/src/updating/client/ClientUpdater.cpp.o CMakeFiles/PvPHero.dir/src/updating/launcher/LauncherUpdater.cpp.o CMakeFiles/PvPHero.dir/src/filesystem/InMemoryFileSystem.cpp.o CMakeFiles/PvPHero.dir/src/external/sago/platform_folders.cpp.o CMakeFiles/PvPHero.dir/src/external/zip/zip.c.o CMakeFiles/PvPHero.dir/src/Assets.cpp.o CMakeFiles/PvPHero.dir/src/main.cpp.o -o PvPHero.app/Contents/MacOS/PvPHero -L/Users/runner/work/pvphero-launcher/pvphero-launcher/build/../deps/AppCore/lib   -L/Users/runner/work/pvphero-launcher/pvphero-launcher/build/../deps/Ultralight/lib   -L/Users/runner/work/pvphero-launcher/pvphero-launcher/build/../deps/UltralightCore/lib   -L/Users/runner/work/pvphero-launcher/pvphero-launcher/build/../deps/WebCore/lib   -L/Users/runner/work/pvphero-launcher/pvphero-launcher/build/../deps/WebKitLibraries/lib -Wl,-rpath,@executable_path/  -lWebCore  -lUltralightCore  -lUltralight  -lAppCore  -lblend2d  -lWTF  -lJavaScriptCore  -lPAL  -lsqlite3  ../deps//WebKitLibraries/lib/libbrotlidec.a  ../deps//WebKitLibraries/lib/libbrotlienc.a  ../deps//WebKitLibraries/lib/libbrotlicommon.a  ../deps//WebKitLibraries/lib/libcrypto.a  ../deps//WebKitLibraries/lib/libcurl.a  ../deps//WebKitLibraries/lib/libfreetype.a  ../deps//WebKitLibraries/lib/libharfbuzz.a  ../deps//WebKitLibraries/lib/libharfbuzz-icu.a  ../deps//WebKitLibraries/lib/libicudata.a  ../deps//WebKitLibraries/lib/libicui18n.a  ../deps//WebKitLibraries/lib/libicuuc.a  ../deps//WebKitLibraries/lib/libjpeg.a  ../deps//WebKitLibraries/lib/libnghttp2_static.a  ../deps//WebKitLibraries/lib/libpng.a  ../deps//WebKitLibraries/lib/libpng16.a  ../deps//WebKitLibraries/lib/libssl.a  ../deps//WebKitLibraries/lib/libtls.a  ../deps//WebKitLibraries/lib/libxml2.a  ../deps//WebKitLibraries/lib/libxslt.a  ../deps//WebKitLibraries/lib/libz.a  -lpthread  -lbmalloc  -ldl  -lbz2  -framework  Accelerate  -framework  AppKit  -framework  SystemConfiguration  -framework  Security && :
clang: error: no such file or directory: '/SUBSYSTEM:CONSOLE'
ninja: build stopped: subcommand failed.
```

@Jonatino you have a clear indication of what's wrong in your case:

```clang: error: no such file or directory: '/SUBSYSTEM:CONSOLE'```

> @jonesmz @jimis @schultetwin1 have you guys figured this out?

Please file separate issues for irrelevant problems.
@jimis thats obvious. The problem is that github CI does not fail because that exit code is hidden because its a ninja subproces... which is exactly what this issue is reporting.
@Jonatino ninja exits with its own error code in case of failure. It is your bash script that hides that. Try adding `set -e` at the beginning of your script and it will exit on the first error. It has nothing to do with this ticket or in general with ninja.

I've been hit hard with this when ccache started `SIGBUS`-ing on me and I had no way of figuring out why all the compilation jobs are suddenly failing with no output.
> @jonatino ninja exits with its own error code in case of failure. It is your bash script that hides that. Try adding `set -e` at the beginning of your script and it will exit on the first error. It has nothing to do with this ticket or in general with ninja.

I have set -e set, check for the exit status of ninja and return it, and yet my script continues on as if ninja's exit code were zero. I'm pretty sure this ticket is valid.
I am trying to address it in https://github.com/ninja-build/ninja/pull/2540



Fix (patch):
diff --git a/src/build.cc b/src/build.cc
index d256d940b0..9411824f3a 100644
--- a/src/build.cc
+++ b/src/build.cc
@@ -34,12 +34,12 @@
 #include "depfile_parser.h"
 #include "deps_log.h"
 #include "disk_interface.h"
+#include "exit_status.h"
 #include "explanations.h"
 #include "graph.h"
 #include "metrics.h"
 #include "state.h"
 #include "status.h"
-#include "subprocess.h"
 #include "util.h"
 
 using namespace std;
@@ -687,7 +687,7 @@ bool Builder::AlreadyUpToDate() const {
   return !plan_.more_to_do();
 }
 
-bool Builder::Build(string* err) {
+ExitStatus Builder::Build(string* err) {
   assert(!AlreadyUpToDate());
   plan_.PrepareQueue();
 
@@ -726,14 +726,14 @@ bool Builder::Build(string* err) {
         if (!StartEdge(edge, err)) {
           Cleanup();
           status_->BuildFinished();
-          return false;
+          return ExitFailure;
         }
 
         if (edge->is_phony()) {
           if (!plan_.EdgeFinished(edge, Plan::kEdgeSucceeded, err)) {
             Cleanup();
             status_->BuildFinished();
-            return false;
+            return ExitFailure;
           }
         } else {
           ++pending_commands;
@@ -760,14 +760,16 @@ bool Builder::Build(string* err) {
         Cleanup();
         status_->BuildFinished();
         *err = "interrupted by user";
-        return false;
+        return result.status;
       }
 
       --pending_commands;
-      if (!FinishCommand(&result, err)) {
+      bool command_finished = FinishCommand(&result, err);
+      SetFailureCode(result.status);
+      if (!command_finished) {
         Cleanup();
         status_->BuildFinished();
-        return false;
+        return result.status;
       }
 
       if (!result.success()) {
@@ -791,11 +793,11 @@ bool Builder::Build(string* err) {
     else
       *err = "stuck [this is a bug]";
 
-    return false;
+    return GetExitCode();
   }
 
   status_->BuildFinished();
-  return true;
+  return ExitSuccess;
 }
 
 bool Builder::StartEdge(Edge* edge, string* err) {
@@ -883,7 +885,7 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) {
   running_edges_.erase(it);
 
   status_->BuildEdgeFinished(edge, start_time_millis, end_time_millis,
-                             result->success(), result->output);
+                             result->status, result->output);
 
   // The rest of this function only applies to successful commands.
   if (!result->success()) {
@@ -1036,3 +1038,10 @@ bool Builder::LoadDyndeps(Node* node, string* err) {
 
   return true;
 }
+
+void Builder::SetFailureCode(ExitStatus code) {
+  // ExitSuccess should not overwrite any error
+  if (code != ExitSuccess) {
+    exit_code_ = code;
+  }
+}
diff --git a/src/build.h b/src/build.h
index ba39e7728a..04e007627f 100644
--- a/src/build.h
+++ b/src/build.h
@@ -209,9 +209,9 @@ struct Builder {
   /// Returns true if the build targets are already up to date.
   bool AlreadyUpToDate() const;
 
-  /// Run the build.  Returns false on error.
+  /// Run the build.  Returns ExitStatus or the exit code of the last failed job.
   /// It is an error to call this function when AlreadyUpToDate() is true.
-  bool Build(std::string* err);
+  ExitStatus Build(std::string* err);
 
   bool StartEdge(Edge* edge, std::string* err);
 
@@ -233,6 +233,10 @@ struct Builder {
   std::unique_ptr<CommandRunner> command_runner_;
   Status* status_;
 
+  /// Returns ExitStatus or the exit code of the last failed job
+  /// (doesn't need to be an enum value of ExitStatus)
+  ExitStatus GetExitCode() const { return exit_code_; }
+
  private:
   bool ExtractDeps(CommandRunner::Result* result, const std::string& deps_type,
                    const std::string& deps_prefix,
@@ -253,6 +257,10 @@ struct Builder {
 
   DependencyScan scan_;
 
+  /// Keep the global exit code for the build
+  ExitStatus exit_code_ = ExitSuccess;
+  void SetFailureCode(ExitStatus code);
+
   // Unimplemented copy ctor and operator= ensure we don't copy the auto_ptr.
   Builder(const Builder &other);        // DO NOT IMPLEMENT
   void operator=(const Builder &other); // DO NOT IMPLEMENT
diff --git a/src/exit_status.h b/src/exit_status.h
index a714ece791..73f9470b46 100644
--- a/src/exit_status.h
+++ b/src/exit_status.h
@@ -15,10 +15,19 @@
 #ifndef NINJA_EXIT_STATUS_H_
 #define NINJA_EXIT_STATUS_H_
 
-enum ExitStatus {
-  ExitSuccess,
+// The underlying type of the ExitStatus enum, used to represent a platform-specific
+// process exit code.
+#ifdef _WIN32
+#define EXIT_STATUS_TYPE unsigned long
+#else  // !_WIN32
+#define EXIT_STATUS_TYPE int
+#endif  // !_WIN32
+
+
+enum ExitStatus : EXIT_STATUS_TYPE {
+  ExitSuccess=0,
   ExitFailure,
-  ExitInterrupted
+  ExitInterrupted=130,
 };
 
 #endif  // NINJA_EXIT_STATUS_H_
diff --git a/src/ninja.cc b/src/ninja.cc
index f681bfec11..119e7d4467 100644
--- a/src/ninja.cc
+++ b/src/ninja.cc
@@ -42,8 +42,8 @@
 #include "clean.h"
 #include "command_collector.h"
 #include "debug_flags.h"
-#include "depfile_parser.h"
 #include "disk_interface.h"
+#include "exit_status.h"
 #include "graph.h"
 #include "graphviz.h"
 #include "json.h"
@@ -165,7 +165,7 @@ struct NinjaMain : public BuildLogUser {
 
   /// Build the targets listed on the command line.
   /// @return an exit code.
-  int RunBuild(int argc, char** argv, Status* status);
+  ExitStatus RunBuild(int argc, char** argv, Status* status);
 
   /// Dump the output requested by '-d stats'.
   void DumpMetrics();
@@ -281,7 +281,7 @@ bool NinjaMain::RebuildManifest(const char* input_file, string* err,
   if (builder.AlreadyUpToDate())
     return false;  // Not an error, but we didn't rebuild.
 
-  if (!builder.Build(err))
+  if (builder.Build(err) != ExitSuccess)
     return false;
 
   // The manifest was only rebuilt if it is now dirty (it may have been cleaned
@@ -1541,12 +1541,12 @@ bool NinjaMain::EnsureBuildDirExists() {
   return true;
 }
 
-int NinjaMain::RunBuild(int argc, char** argv, Status* status) {
+ExitStatus NinjaMain::RunBuild(int argc, char** argv, Status* status) {
   string err;
   vector<Node*> targets;
   if (!CollectTargetsFromArgs(argc, argv, &targets, &err)) {
     status->Error("%s", err.c_str());
-    return 1;
+    return ExitFailure;
   }
 
   disk_interface_.AllowStatCache(g_experimental_statcache);
@@ -1557,7 +1557,7 @@ int NinjaMain::RunBuild(int argc, char** argv, Status* status) {
     if (!builder.AddTarget(targets[i], &err)) {
       if (!err.empty()) {
         status->Error("%s", err.c_str());
-        return 1;
+        return ExitFailure;
       } else {
         // Added a target that is already up-to-date; not really
         // an error.
@@ -1572,18 +1572,18 @@ int NinjaMain::RunBuild(int argc, char** argv, Status* status) {
     if (config_.verbosity != BuildConfig::NO_STATUS_UPDATE) {
       status->Info("no work to do.");
     }
-    return 0;
+    return ExitSuccess;
   }
 
-  if (!builder.Build(&err)) {
+  ExitStatus exit_status = builder.Build(&err);
+  if (exit_status != ExitSuccess) {
     status->Info("build stopped: %s.", err.c_str());
     if (err.find("interrupted by user") != string::npos) {
-      return 130;
+      return ExitInterrupted;
     }
-    return 1;
   }
 
-  return 0;
+  return exit_status;
 }
 
 #ifdef _MSC_VER
@@ -1801,7 +1801,7 @@ NORETURN void real_main(int argc, char** argv) {
 
     ninja.ParsePreviousElapsedTimes();
 
-    int result = ninja.RunBuild(argc, argv, status);
+    ExitStatus result = ninja.RunBuild(argc, argv, status);
     if (g_metrics)
       ninja.DumpMetrics();
     exit(result);
diff --git a/src/status.h b/src/status.h
index 29db7c203a..a873993446 100644
--- a/src/status.h
+++ b/src/status.h
@@ -16,6 +16,7 @@
 #define NINJA_STATUS_H_
 
 #include <string>
+#include "exit_status.h"
 
 struct BuildConfig;
 struct Edge;
@@ -29,7 +30,7 @@ struct Status {
   virtual void BuildEdgeStarted(const Edge* edge,
                                 int64_t start_time_millis) = 0;
   virtual void BuildEdgeFinished(Edge* edge, int64_t start_time_millis,
-                                 int64_t end_time_millis, bool success,
+                                 int64_t end_time_millis, ExitStatus exit_code,
                                  const std::string& output) = 0;
   virtual void BuildStarted() = 0;
   virtual void BuildFinished() = 0;
diff --git a/src/status_printer.cc b/src/status_printer.cc
index 8db6809561..e69cd15a40 100644
--- a/src/status_printer.cc
+++ b/src/status_printer.cc
@@ -33,6 +33,7 @@
 
 #include "build.h"
 #include "debug_flags.h"
+#include "exit_status.h"
 
 using namespace std;
 
@@ -174,7 +175,7 @@ void StatusPrinter::RecalculateProgressPrediction() {
 }
 
 void StatusPrinter::BuildEdgeFinished(Edge* edge, int64_t start_time_millis,
-                                      int64_t end_time_millis, bool success,
+                                      int64_t end_time_millis, ExitStatus exit_code,
                                       const string& output) {
   time_millis_ = end_time_millis;
   ++finished_edges_;
@@ -202,16 +203,17 @@ void StatusPrinter::BuildEdgeFinished(Edge* edge, int64_t start_time_millis,
   --running_edges_;
 
   // Print the command that is spewing before printing its output.
-  if (!success) {
+  if (exit_code != ExitSuccess) {
     string outputs;
     for (vector<Node*>::const_iterator o = edge->outputs_.begin();
          o != edge->outputs_.end(); ++o)
       outputs += (*o)->path() + " ";
 
+    string failed = "FAILED: [code=" + std::to_string(exit_code) + "] ";
     if (printer_.supports_color()) {
-        printer_.PrintOnNewLine("\x1B[31m" "FAILED: " "\x1B[0m" + outputs + "\n");
+        printer_.PrintOnNewLine("\x1B[31m" + failed + "\x1B[0m" + outputs + "\n");
     } else {
-        printer_.PrintOnNewLine("FAILED: " + outputs + "\n");
+        printer_.PrintOnNewLine(failed + outputs + "\n");
     }
     printer_.PrintOnNewLine(edge->EvaluateCommand() + "\n");
   }
diff --git a/src/status_printer.h b/src/status_printer.h
index 08a8d1a93d..213e9ce825 100644
--- a/src/status_printer.h
+++ b/src/status_printer.h
@@ -16,6 +16,7 @@
 #include <cstdint>
 #include <queue>
 
+#include "exit_status.h"
 #include "explanations.h"
 #include "line_printer.h"
 #include "status.h"
@@ -31,7 +32,7 @@ struct StatusPrinter : Status {
 
   void BuildEdgeStarted(const Edge* edge, int64_t start_time_millis) override;
   void BuildEdgeFinished(Edge* edge, int64_t start_time_millis,
-                                 int64_t end_time_millis, bool success,
+                                 int64_t end_time_millis, ExitStatus exit_code,
                                  const std::string& output) override;
   void BuildStarted() override;
   void BuildFinished() override;
diff --git a/src/subprocess-posix.cc b/src/subprocess-posix.cc
index 8e785406c9..4d8da5606f 100644
--- a/src/subprocess-posix.cc
+++ b/src/subprocess-posix.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "exit_status.h"
 #include "subprocess.h"
 
 #include <sys/select.h>
@@ -165,15 +166,16 @@ ExitStatus Subprocess::Finish() {
 #endif
 
   if (WIFEXITED(status)) {
-    int exit = WEXITSTATUS(status);
-    if (exit == 0)
-      return ExitSuccess;
-  } else if (WIFSIGNALED(status)) {
+    // propagate the status transparently
+    return static_cast<ExitStatus>(WEXITSTATUS(status));
+  }
+  if (WIFSIGNALED(status)) {
     if (WTERMSIG(status) == SIGINT || WTERMSIG(status) == SIGTERM
         || WTERMSIG(status) == SIGHUP)
       return ExitInterrupted;
   }
-  return ExitFailure;
+  // At this point, we exit with any other signal+128
+  return static_cast<ExitStatus>(status + 128);
 }
 
 bool Subprocess::Done() const {
diff --git a/src/subprocess-win32.cc b/src/subprocess-win32.cc
index ff3baaca7f..4cb8472141 100644
--- a/src/subprocess-win32.cc
+++ b/src/subprocess-win32.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "exit_status.h"
 #include "subprocess.h"
 
 #include <assert.h>
@@ -198,9 +199,8 @@ ExitStatus Subprocess::Finish() {
   CloseHandle(child_);
   child_ = NULL;
 
-  return exit_code == 0              ? ExitSuccess :
-         exit_code == CONTROL_C_EXIT ? ExitInterrupted :
-                                       ExitFailure;
+  return exit_code == CONTROL_C_EXIT ? ExitInterrupted :
+                                       static_cast<ExitStatus>(exit_code);
 }
 
 bool Subprocess::Done() const {