From 0f555577288a1c4a672bc7121904dfc092b390f0 Mon Sep 17 00:00:00 2001 From: Jan Niklas Hasse Date: Wed, 13 May 2026 21:06:00 +0200 Subject: [PATCH] Add --status flag with Ninja-style variable expansion Introduces a `--status FMT` command-line flag that configures the progress status using Ninja's regular `$var`/`${var}` syntax with descriptive variable names ($finished, $total, $progress, $elapsed, etc.) instead of the `%`-escapes used by `NINJA_STATUS`. When passed, it takes precedence over `NINJA_STATUS`; the env-var path is left unchanged for backwards compatibility. --- .gitignore | 2 +- doc/manual.asciidoc | 24 +++++++++ misc/output_test.py | 12 +++++ src/build.h | 3 ++ src/ninja.cc | 8 ++- src/status_printer.cc | 115 ++++++++++++++++++++++++++++++++++++++++-- src/status_printer.h | 14 ++++- 7 files changed, 170 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index bb13dcd3..b54d20df 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ *.pdb *.ilk /build*/ -/build.ninja +/build*.ninja /ninja /ninja.bootstrap /build_log_perftest diff --git a/doc/manual.asciidoc b/doc/manual.asciidoc index 718a4302..0d4a4a78 100644 --- a/doc/manual.asciidoc +++ b/doc/manual.asciidoc @@ -248,6 +248,30 @@ The default progress status is `"[%f/%t] "` (note the trailing space to separate from the build rule). Another example of possible progress status could be `"[%u/%r/%f] "`. +The `--status FMT` command-line flag also configures the progress status, +but uses Ninja's regular variable-expansion syntax (`$var` or `${var}`) +instead of `%`-escapes, with descriptive variable names. When passed, +it takes precedence over `NINJA_STATUS`. + +The available variables and their `NINJA_STATUS` equivalents are: + +`$started`:: `%s` -- The number of started edges. +`$total`:: `%t` -- The total number of edges. +`$progress`:: `%p` -- The percentage of finished edges. +`$running`:: `%r` -- The number of currently running edges. +`$remaining`:: `%u` -- The number of remaining edges to start. +`$finished`:: `%f` -- The number of finished edges. +`$rate`:: `%o` -- Overall rate of finished edges per second. +`$current_rate`:: `%c` -- Current rate of finished edges per second. +`$elapsed_seconds`:: `%e` -- Elapsed time in seconds. +`$eta_seconds`:: `%E` -- Remaining time (ETA) in seconds. +`$elapsed`:: `%w` -- Elapsed time in [h:]mm:ss format. +`$eta`:: `%W` -- Remaining time (ETA) in [h:]mm:ss format. +`$predicted_progress`:: `%P` -- Percentage (in `ppp%` format) of time elapsed +out of predicted total runtime. + +To produce a literal `$`, use `$$`. The `--status` flag was added in Ninja 1.14. + If `MAKEFLAGS` is defined in the environment, if may alter how Ninja dispatches parallel build commands. See the GNU Jobserver support section for details. diff --git a/misc/output_test.py b/misc/output_test.py index 40a9ac9f..03b6b2c9 100755 --- a/misc/output_test.py +++ b/misc/output_test.py @@ -385,6 +385,18 @@ ninja: build stopped: subcommand failed. output = run(Output.BUILD_SIMPLE_ECHO, flags='--quiet') self.assertEqual(output, 'do thing\n') + def test_status_flag(self) -> None: + 'Does --status accept a Ninja-style $-format?' + output = run(Output.BUILD_SIMPLE_ECHO, + flags="--status '<${finished}/${total}> '") + self.assertEqual(output, '<1/1> echo a\x1b[K\ndo thing\n') + + def test_status_flag_unknown_variable(self) -> None: + 'Does --status fail clearly on an unknown variable?' + self._test_expected_error( + Output.BUILD_SIMPLE_ECHO, "--status '$nope '", + "ninja: fatal: unknown variable 'nope' in --status format\n") + def test_entering_directory_on_stdout(self) -> None: output = run(Output.BUILD_SIMPLE_ECHO, flags='-C$PWD', pipe=True) self.assertEqual(output.splitlines()[0][:25], "ninja: Entering directory") diff --git a/src/build.h b/src/build.h index 4b6e4e84..571e08f4 100644 --- a/src/build.h +++ b/src/build.h @@ -194,6 +194,9 @@ struct BuildConfig { /// The maximum load average we must not exceed. A negative value /// means that we do not have any limit. double max_load_average = -0.0f; + /// Progress status format, as set by --status. Overrides $NINJA_STATUS + /// when non-null. + const char* progress_status_format = nullptr; DepfileParserOptions depfile_parser_options; }; diff --git a/src/ninja.cc b/src/ninja.cc index 2f11a0f5..a57cd0a3 100644 --- a/src/ninja.cc +++ b/src/ninja.cc @@ -234,6 +234,8 @@ void Usage(const BuildConfig& config) { " --version print ninja version (\"%s\")\n" " -v, --verbose show all command lines while building\n" " --quiet don't show progress status, just command output\n" +" --status FMT progress status format using Ninja-style $vars\n" +" (e.g. --status '[$finished/$total] ')\n" "\n" " -C DIR change to DIR before doing anything else\n" " -f FILE specify input build file [default=build.ninja]\n" @@ -1729,12 +1731,13 @@ int ReadFlags(int* argc, char*** argv, Options* options, BuildConfig* config) { DeferGuessParallelism deferGuessParallelism(config); - enum { OPT_VERSION = 1, OPT_QUIET = 2 }; + enum { OPT_VERSION = 1, OPT_QUIET = 2, OPT_STATUS = 3 }; const option kLongOptions[] = { { "help", no_argument, NULL, 'h' }, { "version", no_argument, NULL, OPT_VERSION }, { "verbose", no_argument, NULL, 'v' }, { "quiet", no_argument, NULL, OPT_QUIET }, + { "status", required_argument, NULL, OPT_STATUS }, { NULL, 0, NULL, 0 } }; @@ -1800,6 +1803,9 @@ int ReadFlags(int* argc, char*** argv, case OPT_QUIET: config->verbosity = BuildConfig::NO_STATUS_UPDATE; break; + case OPT_STATUS: + config->progress_status_format = optarg; + break; case 'w': if (!WarningEnable(optarg, options)) return 1; diff --git a/src/status_printer.cc b/src/status_printer.cc index 407a5818..512fafd1 100644 --- a/src/status_printer.cc +++ b/src/status_printer.cc @@ -34,9 +34,23 @@ #include "build.h" #include "debug_flags.h" #include "exit_status.h" +#include "lexer.h" +#include "util.h" using namespace std; +namespace { +/// Env that resolves variables in a `--status` format string by asking +/// the StatusPrinter for their current value. +struct StatusFormatEnv : public Env { + const StatusPrinter* printer; + explicit StatusFormatEnv(const StatusPrinter* p) : printer(p) {} + string LookupVariable(const string& var) override { + return printer->FormatStatusVariable(var); + } +}; +} // namespace + Status* Status::factory(const BuildConfig& config) { return new StatusPrinter(config); } @@ -49,9 +63,22 @@ StatusPrinter::StatusPrinter(const BuildConfig& config) if (config_.verbosity != BuildConfig::NORMAL) printer_.set_smart_terminal(false); - progress_status_format_ = getenv("NINJA_STATUS"); - if (!progress_status_format_) - progress_status_format_ = "[%f/%t] "; + if (config.progress_status_format) { + // --status uses Ninja-style variable expansion ($var / ${var}). + // Append a newline because Lexer::ReadVarValue terminates on \n. + string input = string(config.progress_status_format) + "\n"; + Lexer lexer; + lexer.Start("--status", input); + status_eval_.reset(new EvalString()); + string err; + if (!lexer.ReadVarValue(status_eval_.get(), &err)) + Fatal("invalid --status: %s", err.c_str()); + progress_status_format_ = NULL; + } else { + progress_status_format_ = getenv("NINJA_STATUS"); + if (!progress_status_format_) + progress_status_format_ = "[%f/%t] "; + } } void StatusPrinter::EdgeAddedToPlan(const Edge* edge) { @@ -405,6 +432,79 @@ string StatusPrinter::FormatProgressStatus(const char* progress_status_format, return out; } +string StatusPrinter::FormatStatusVariable(const string& name) const { + char buf[32]; + + if (name == "started") { + snprintf(buf, sizeof(buf), "%d", started_edges_); + return buf; + } + if (name == "total") { + snprintf(buf, sizeof(buf), "%d", total_edges_); + return buf; + } + if (name == "running") { + snprintf(buf, sizeof(buf), "%d", running_edges_); + return buf; + } + if (name == "remaining") { + snprintf(buf, sizeof(buf), "%d", total_edges_ - started_edges_); + return buf; + } + if (name == "finished") { + snprintf(buf, sizeof(buf), "%d", finished_edges_); + return buf; + } + if (name == "rate") { + SnprintfRate(finished_edges_ / (time_millis_ / 1e3), buf, "%.1f"); + return buf; + } + if (name == "current_rate") { + current_rate_.UpdateRate(finished_edges_, time_millis_); + SnprintfRate(current_rate_.rate(), buf, "%.1f"); + return buf; + } + if (name == "progress") { + int percent = 0; + if (finished_edges_ != 0 && total_edges_ != 0) + percent = (100 * finished_edges_) / total_edges_; + snprintf(buf, sizeof(buf), "%3i%%", percent); + return buf; + } + if (name == "predicted_progress") { + snprintf(buf, sizeof(buf), "%3i%%", + (int)(100. * time_predicted_percentage_)); + return buf; + } + + if (name == "elapsed" || name == "elapsed_seconds" || + name == "eta" || name == "eta_seconds") { + double elapsed_sec = time_millis_ / 1e3; + double eta_sec = -1; + if (time_predicted_percentage_ != 0.0) { + double total_wall_time = time_millis_ / time_predicted_percentage_; + eta_sec = (total_wall_time - time_millis_) / 1e3; + } + const bool print_with_hours = + elapsed_sec >= 60 * 60 || eta_sec >= 60 * 60; + const bool is_eta = (name == "eta" || name == "eta_seconds"); + double sec = is_eta ? eta_sec : elapsed_sec; + if (sec < 0) + return "?"; + if (name == "elapsed_seconds" || name == "eta_seconds") { + snprintf(buf, sizeof(buf), "%.3f", sec); + } else if (print_with_hours) { + snprintf(buf, sizeof(buf), FORMAT_TIME_HMMSS((int64_t)sec)); + } else { + snprintf(buf, sizeof(buf), FORMAT_TIME_MMSS((int64_t)sec)); + } + return buf; + } + + Fatal("unknown variable '%s' in --status format", name.c_str()); + return ""; +} + void StatusPrinter::PrintStatus(const Edge* edge, int64_t time_millis) { if (explanations_) { explanations_->ExplainEdge(edge); @@ -422,8 +522,13 @@ void StatusPrinter::PrintStatus(const Edge* edge, int64_t time_millis) { if (to_print.empty() || force_full_command) to_print = edge->GetBinding("command"); - to_print = FormatProgressStatus(progress_status_format_, time_millis) - + to_print; + if (status_eval_) { + StatusFormatEnv env(this); + to_print = status_eval_->Evaluate(&env) + to_print; + } else { + to_print = FormatProgressStatus(progress_status_format_, time_millis) + + to_print; + } printer_.Print(to_print, force_full_command ? LinePrinter::FULL : LinePrinter::ELIDE); diff --git a/src/status_printer.h b/src/status_printer.h index af5bdca0..424678e8 100644 --- a/src/status_printer.h +++ b/src/status_printer.h @@ -14,8 +14,10 @@ #pragma once #include +#include #include +#include "eval_env.h" #include "exit_status.h" #include "explanations.h" #include "line_printer.h" @@ -50,6 +52,11 @@ struct StatusPrinter : Status { std::string FormatProgressStatus(const char* progress_status_format, int64_t time_millis) const; + /// Look up the value of a named status variable (used by `--status`, + /// which evaluates a Ninja-style format string). Calls Fatal on an + /// unknown name. + std::string FormatStatusVariable(const std::string& name) const; + /// Set the |explanations_| pointer. Used to implement `-d explain`. void SetExplanations(Explanations* explanations) override { explanations_ = explanations; @@ -92,9 +99,14 @@ struct StatusPrinter : Status { /// An optional Explanations pointer, used to implement `-d explain`. Explanations* explanations_ = nullptr; - /// The custom progress status format to use. + /// The custom progress status format to use (NINJA_STATUS or default). + /// Null when `--status` is in effect; in that case `status_eval_` is used. const char* progress_status_format_; + /// Parsed `--status` format, evaluated against status variables on each + /// update. Null when `--status` was not supplied. + std::unique_ptr status_eval_; + template void SnprintfRate(double rate, char (&buf)[S], const char* format) const { if (rate == -1)