From 280aa46f240ceb554401f5cece52e4d4a4d6d416 Mon Sep 17 00:00:00 2001 From: Marc Delorme Date: Thu, 16 May 2024 14:29:14 +0200 Subject: [PATCH 1/4] Let depslog store outputs This is to support dynamically computed outputs for the 'dynout' attribute --- src/build.cc | 2 +- src/deps_log.cc | 25 ++++++++++++++++++------- src/deps_log.h | 9 +++++---- src/deps_log_test.cc | 7 +++++-- src/graph.cc | 4 ++-- src/missing_deps_test.cc | 2 +- 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/build.cc b/src/build.cc index deb8f04c8b..1a41a13cf7 100644 --- a/src/build.cc +++ b/src/build.cc @@ -1022,7 +1022,7 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) { TimeStamp deps_mtime = disk_interface_->Stat((*o)->path(), err); if (deps_mtime == -1) return false; - if (!scan_.deps_log()->RecordDeps(*o, deps_mtime, deps_nodes)) { + if (!scan_.deps_log()->RecordDeps(*o, deps_mtime, deps_nodes, 0)) { *err = std::string("Error writing to deps log: ") + strerror(errno); return false; } diff --git a/src/deps_log.cc b/src/deps_log.cc index b28736ad3d..af1b4f1300 100644 --- a/src/deps_log.cc +++ b/src/deps_log.cc @@ -35,7 +35,7 @@ using namespace std; // The version is stored as 4 bytes after the signature and also serves as a // byte order mark. Signature and version combined are 16 bytes long. const char kFileSignature[] = "# ninjadeps\n"; -const int kCurrentVersion = 4; +const int kCurrentVersion = 5; // Record size is currently limited to less than the full 32 bit, due to // internal buffers having to have this size. @@ -58,13 +58,13 @@ bool DepsLog::OpenForWrite(const string& path, string* err) { } bool DepsLog::RecordDeps(Node* node, TimeStamp mtime, - const vector& nodes) { - return RecordDeps(node, mtime, nodes.size(), + const vector& nodes, int outputs_count) { + return RecordDeps(node, mtime, nodes.size(), outputs_count, nodes.empty() ? NULL : const_cast(&nodes.front())); } bool DepsLog::RecordDeps(Node* node, TimeStamp mtime, - int node_count, Node** nodes) { + int node_count, int outputs_count, Node** nodes) { // Track whether there's any new data to be recorded. bool made_change = false; @@ -116,6 +116,8 @@ bool DepsLog::RecordDeps(Node* node, TimeStamp mtime, size |= 0x80000000; // Deps record: set high bit. if (fwrite(&size, 4, 1, file_) < 1) return false; + if (fwrite(&outputs_count, 4, 1, file_) < 1) + return false; int id = node->id(); if (fwrite(&id, 4, 1, file_) < 1) return false; @@ -134,7 +136,7 @@ bool DepsLog::RecordDeps(Node* node, TimeStamp mtime, return false; // Update in-memory representation. - Deps* deps = new Deps(mtime, node_count); + Deps* deps = new Deps(mtime, node_count, outputs_count); for (int i = 0; i < node_count; ++i) deps->nodes[i] = nodes[i]; UpdateDeps(node->id(), deps); @@ -197,6 +199,15 @@ LoadStatus DepsLog::Load(const string& path, State* state, string* err) { bool is_deps = (size >> 31) != 0; size = size & 0x7FFFFFFF; + int outputs_count = 0; + if (is_deps) { + if (fread(&outputs_count, 4, 1, f) < 1) { + if (!feof(f)) + read_failed = true; + break; + } + } + if (size > kMaxRecordSize || fread(buf, size, 1, f) < 1) { read_failed = true; break; @@ -212,7 +223,7 @@ LoadStatus DepsLog::Load(const string& path, State* state, string* err) { deps_data += 3; int deps_count = (size / 4) - 3; - Deps* deps = new Deps(mtime, deps_count); + Deps* deps = new Deps(mtime, deps_count, outputs_count); for (int i = 0; i < deps_count; ++i) { assert(deps_data[i] < (int)nodes_.size()); assert(nodes_[deps_data[i]]); @@ -336,7 +347,7 @@ bool DepsLog::Recompact(const string& path, string* err) { continue; if (!new_log.RecordDeps(nodes_[old_id], deps->mtime, - deps->node_count, deps->nodes)) { + deps->node_count, deps->outputs_count, deps->nodes)) { new_log.Close(); return false; } diff --git a/src/deps_log.h b/src/deps_log.h index 2a1b188906..ba5e865fb3 100644 --- a/src/deps_log.h +++ b/src/deps_log.h @@ -71,17 +71,18 @@ struct DepsLog { // Writing (build-time) interface. bool OpenForWrite(const std::string& path, std::string* err); - bool RecordDeps(Node* node, TimeStamp mtime, const std::vector& nodes); - bool RecordDeps(Node* node, TimeStamp mtime, int node_count, Node** nodes); + bool RecordDeps(Node* node, TimeStamp mtime, const std::vector& nodes, int outputs_count = 0); + bool RecordDeps(Node* node, TimeStamp mtime, int node_count, int outputs_count, Node** nodes); void Close(); // Reading (startup-time) interface. struct Deps { - Deps(int64_t mtime, int node_count) - : mtime(mtime), node_count(node_count), nodes(new Node*[node_count]) {} + Deps(int64_t mtime, int node_count, int outputs_count) + : mtime(mtime), node_count(node_count), outputs_count(outputs_count), nodes(new Node*[node_count]) {} ~Deps() { delete [] nodes; } TimeStamp mtime; int node_count; + int outputs_count; Node** nodes; }; LoadStatus Load(const std::string& path, State* state, std::string* err); diff --git a/src/deps_log_test.cc b/src/deps_log_test.cc index cb1c925532..705f17c784 100644 --- a/src/deps_log_test.cc +++ b/src/deps_log_test.cc @@ -50,7 +50,8 @@ TEST_F(DepsLogTest, WriteRead) { vector deps; deps.push_back(state1.GetNode("foo.h", 0)); deps.push_back(state1.GetNode("bar.h", 0)); - log1.RecordDeps(state1.GetNode("out.o", 0), 1, deps); + deps.push_back(state1.GetNode("out.bis", 0)); + log1.RecordDeps(state1.GetNode("out.o", 0), 1, deps, 1); deps.clear(); deps.push_back(state1.GetNode("foo.h", 0)); @@ -60,9 +61,11 @@ TEST_F(DepsLogTest, WriteRead) { DepsLog::Deps* log_deps = log1.GetDeps(state1.GetNode("out.o", 0)); ASSERT_TRUE(log_deps); ASSERT_EQ(1, log_deps->mtime); - ASSERT_EQ(2, log_deps->node_count); + ASSERT_EQ(3, log_deps->node_count); + ASSERT_EQ(1, log_deps->outputs_count); ASSERT_EQ("foo.h", log_deps->nodes[0]->path()); ASSERT_EQ("bar.h", log_deps->nodes[1]->path()); + ASSERT_EQ("out.bis", log_deps->nodes[2]->path()); } log1.Close(); diff --git a/src/graph.cc b/src/graph.cc index 143eabdfb4..76ecfb0a6e 100644 --- a/src/graph.cc +++ b/src/graph.cc @@ -763,8 +763,8 @@ bool ImplicitDepLoader::LoadDepsFromLog(Edge* edge, string* err) { } vector::iterator implicit_dep = - PreallocateSpace(edge, deps->node_count); - for (int i = 0; i < deps->node_count; ++i, ++implicit_dep) { + PreallocateSpace(edge, deps->node_count - deps->outputs_count); + for (int i = 0; i < (deps->node_count - deps->outputs_count); ++i, ++implicit_dep) { Node* node = deps->nodes[i]; *implicit_dep = node; node->AddOutEdge(edge); diff --git a/src/missing_deps_test.cc b/src/missing_deps_test.cc index dae377b49d..8b8a84595d 100644 --- a/src/missing_deps_test.cc +++ b/src/missing_deps_test.cc @@ -45,7 +45,7 @@ struct MissingDependencyScannerTest : public testing::Test { void RecordDepsLogDep(const std::string& from, const std::string& to) { Node* node_deps[] = { state_.LookupNode(to) }; - deps_log_.RecordDeps(state_.LookupNode(from), 0, 1, node_deps); + deps_log_.RecordDeps(state_.LookupNode(from), 0, 1, 0, node_deps); } void ProcessAllNodes() { From ca1cbd39b4c337d9f05cb5575f0dcaf5592dd1a1 Mon Sep 17 00:00:00 2001 From: Marc Delorme Date: Thu, 16 May 2024 14:33:25 +0200 Subject: [PATCH 2/4] Add parser for dynout files Co-authored-by: Hampus Adolfsson --- CMakeLists.txt | 2 ++ configure.py | 1 + src/dynout_parser.cc | 41 +++++++++++++++++++++++++ src/dynout_parser.h | 32 ++++++++++++++++++++ src/dynout_parser_test.cc | 64 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 src/dynout_parser.cc create mode 100644 src/dynout_parser.h create mode 100644 src/dynout_parser_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index b8fdee7d3a..9966d24565 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,6 +131,7 @@ add_library(libninja OBJECT src/clparser.cc src/dyndep.cc src/dyndep_parser.cc + src/dynout_parser.cc src/debug_flags.cc src/deps_log.cc src/disk_interface.cc @@ -264,6 +265,7 @@ if(BUILD_TESTING) src/deps_log_test.cc src/disk_interface_test.cc src/dyndep_parser_test.cc + src/dynout_parser_test.cc src/edit_distance_test.cc src/explanations_test.cc src/graph_test.cc diff --git a/configure.py b/configure.py index c88daad508..d0ddce251d 100755 --- a/configure.py +++ b/configure.py @@ -538,6 +538,7 @@ def has_re2c() -> bool: 'disk_interface', 'dyndep', 'dyndep_parser', + 'dynout_parser', 'edit_distance', 'eval_env', 'graph', diff --git a/src/dynout_parser.cc b/src/dynout_parser.cc new file mode 100644 index 0000000000..4bc05183c0 --- /dev/null +++ b/src/dynout_parser.cc @@ -0,0 +1,41 @@ +/* Generated by re2c 1.3 */ +// Copyright 2011 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dynout_parser.h" + +#include "metrics.h" +#include "string_piece.h" + +bool DynoutParser::Parse(const std::string& content, + std::vector& result, std::string* err) { + METRIC_RECORD("dynout parse"); + /// Split `content` into a series of individual text lines, without trailing + /// newlines, and remove empty lines + const char* input = content.c_str(); + const char* start = input; + const char* limit = input + content.size(); + while (start < limit) { + const char* end = start; + // Note: \r\n will be treated as a line end + an empty line, + // the latter will be ignored. This simplifies the logic in this + // loop to reduce its code and speed it up. + while (end < limit && (*end != '\n' && *end != '\r')) + ++end; + if (end > start) + result.push_back(StringPiece(start, end - start)); + start = end + 1; + } + return true; +} diff --git a/src/dynout_parser.h b/src/dynout_parser.h new file mode 100644 index 0000000000..b859a0b1e5 --- /dev/null +++ b/src/dynout_parser.h @@ -0,0 +1,32 @@ +// Copyright 2011 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef NINJA_DYNOUT_PARSER_H_ +#define NINJA_DYNOUT_PARSER_H_ + +#include +#include + +#include "string_piece.h" + +/// Parser for dynout file. +struct DynoutParser { + + /// Parse a dynout file, placing each listed output into `result`. + /// The resulting StringPieces point into `content`. + static bool Parse(const std::string& content, + std::vector& result, std::string* err); +}; + +#endif // NINJA_DYNOUT_PARSER_H_ diff --git a/src/dynout_parser_test.cc b/src/dynout_parser_test.cc new file mode 100644 index 0000000000..7f25d12068 --- /dev/null +++ b/src/dynout_parser_test.cc @@ -0,0 +1,64 @@ +// Copyright 2011 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dynout_parser.h" + +#include "test.h" + +using namespace std; + +struct DynoutParserTest : public testing::Test { + + VirtualFileSystem fs_; +}; + +TEST_F(DynoutParserTest, Empty) { + std::string input; + string err; + std::vector result; + EXPECT_TRUE(DynoutParser::Parse(input, result, &err)); + ASSERT_EQ(err, ""); + ASSERT_TRUE(result.empty()); +} + +TEST_F(DynoutParserTest, MultipleEntries) { + std::string input = "file1\nfile2\nfile3"; + string err; + std::vector result; + EXPECT_TRUE(DynoutParser::Parse(input, result, &err)); + ASSERT_EQ(err, ""); + ASSERT_EQ(result[0], "file1"); + ASSERT_EQ(result[1], "file2"); + ASSERT_EQ(result[2], "file3"); +} + +TEST_F(DynoutParserTest, EmptyLines) { + std::string input = "\nfile1\n\n\nfile2\n\n"; + string err; + std::vector result; + EXPECT_TRUE(DynoutParser::Parse(input, result, &err)); + ASSERT_EQ(err, ""); + ASSERT_EQ(result[0], "file1"); + ASSERT_EQ(result[1], "file2"); +} + +TEST_F(DynoutParserTest, CRLF) { + std::string input = "\r\nfile1\r\n\r\nfile2\r\n"; + string err; + std::vector result; + EXPECT_TRUE(DynoutParser::Parse(input, result, &err)); + ASSERT_EQ(err, ""); + ASSERT_EQ(result[0], "file1"); + ASSERT_EQ(result[1], "file2"); +} From c5137cd5ae45d96074fd4b7931b9077f07898ce3 Mon Sep 17 00:00:00 2001 From: Marc Delorme Date: Thu, 16 May 2024 14:43:50 +0200 Subject: [PATCH 3/4] Implement 'dynout' feature to inform ninja about dynamic outputs Co-authored-by: Hampus Adolfsson --- doc/manual.asciidoc | 7 ++ src/build.cc | 81 ++++++++++++++++- src/build.h | 4 + src/build_log.cc | 47 +++++----- src/build_log.h | 4 +- src/build_log_test.cc | 25 +++++ src/build_test.cc | 177 ++++++++++++++++++++++++++++++++++++ src/clean.cc | 29 +++++- src/clean.h | 7 +- src/clean_test.cc | 144 ++++++++++++++++++++++++----- src/debug_flags.cc | 2 + src/debug_flags.h | 2 + src/deps_log.cc | 11 ++- src/eval_env.cc | 1 + src/graph.cc | 144 ++++++++++++++++++++++++++++- src/graph.h | 19 +++- src/graph_test.cc | 85 +++++++++++++++++ src/manifest_parser_test.cc | 1 + src/ninja.cc | 12 ++- 19 files changed, 740 insertions(+), 62 deletions(-) diff --git a/doc/manual.asciidoc b/doc/manual.asciidoc index e8b66807cf..fc52647942 100644 --- a/doc/manual.asciidoc +++ b/doc/manual.asciidoc @@ -900,6 +900,13 @@ keys. stored as `.ninja_deps` in the `builddir`, see <>. +`dynout`:: path to an optional _dynout file_ that contains extra _implicit + outputs_ generated by the rule. This allows ninja to dynamically discover + output files whose presence is decided during the build, so that for + subsequent builds the edge is re-run if some dynamic output is missing, and + dynamic outputs are cleaned when using the `-t clean` tool. The dynout file + syntax expects one path per line. + `msvc_deps_prefix`:: _(Available since Ninja 1.5.)_ defines the string which should be stripped from msvc's /showIncludes output. Only needed when `deps = msvc` and no English Visual Studio version is used. diff --git a/src/build.cc b/src/build.cc index 1a41a13cf7..6a8a404b17 100644 --- a/src/build.cc +++ b/src/build.cc @@ -34,6 +34,7 @@ #include "depfile_parser.h" #include "deps_log.h" #include "disk_interface.h" +#include "dynout_parser.h" #include "explanations.h" #include "graph.h" #include "metrics.h" @@ -698,6 +699,7 @@ void Builder::Cleanup() { for (vector::iterator e = active_edges.begin(); e != active_edges.end(); ++e) { string depfile = (*e)->GetUnescapedDepfile(); + string dynout = (*e)->GetUnescapedDynout(); for (vector::iterator o = (*e)->outputs_.begin(); o != (*e)->outputs_.end(); ++o) { // Only delete this output if it was actually modified. This is @@ -716,6 +718,8 @@ void Builder::Cleanup() { } if (!depfile.empty()) disk_interface_->RemoveFile(depfile); + if (!dynout.empty()) + disk_interface_->RemoveFile(dynout); } } @@ -949,6 +953,18 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) { } } + int outputs_count = 0; + string extract_err; + std::string dynout_file = edge->GetUnescapedDynout(); + if (!ExtractDynouts(edge, dynout_file, &deps_nodes, &outputs_count, + &extract_err) && + result->success()) { + if (!result->output.empty()) + result->output.append("\n"); + result->output.append(extract_err); + result->status = ExitFailure; + } + int64_t start_time_millis, end_time_millis; RunningEdgeMap::iterator it = running_edges_.find(edge); start_time_millis = it->second; @@ -1008,21 +1024,23 @@ bool Builder::FinishCommand(CommandRunner::Result* result, string* err) { disk_interface_->RemoveFile(rspfile); if (scan_.build_log()) { - if (!scan_.build_log()->RecordCommand(edge, start_time_millis, - end_time_millis, record_mtime)) { + std::vector dynouts(deps_nodes.end() - outputs_count, + deps_nodes.end()); + if (!scan_.build_log()->RecordCommand( + edge, start_time_millis, end_time_millis, record_mtime, dynouts)) { *err = string("Error writing to build log: ") + strerror(errno); return false; } } - if (!deps_type.empty() && !config_.dry_run) { + if ((!deps_type.empty() || !dynout_file.empty()) && !config_.dry_run) { assert(!edge->outputs_.empty() && "should have been rejected by parser"); for (std::vector::const_iterator o = edge->outputs_.begin(); o != edge->outputs_.end(); ++o) { TimeStamp deps_mtime = disk_interface_->Stat((*o)->path(), err); if (deps_mtime == -1) return false; - if (!scan_.deps_log()->RecordDeps(*o, deps_mtime, deps_nodes, 0)) { + if (!scan_.deps_log()->RecordDeps(*o, deps_mtime, deps_nodes, outputs_count)) { *err = std::string("Error writing to deps log: ") + strerror(errno); return false; } @@ -1109,3 +1127,58 @@ bool Builder::LoadDyndeps(Node* node, string* err) { return true; } + +bool Builder::ExtractDynouts(Edge* edge, const std::string& dynout_file, + std::vector* nodes, int* outputs_count, + std::string* err) { + if (dynout_file.empty()) { + return true; + } + + // Read depfile content. Treat a missing depfile as empty. + std::string content; + switch (disk_interface_->ReadFile(dynout_file, &content, err)) { + case DiskInterface::Okay: + break; + case DiskInterface::NotFound: + if (err != NULL) { + err->clear(); + } + break; + case DiskInterface::OtherError: + if (err != NULL) { + *err = "loading '" + dynout_file + "': " + *err; + } + return false; + } + + std::vector output_paths; + std::string parse_err; + if (!DynoutParser::Parse(content, output_paths, &parse_err)) { + if (err != NULL) { + *err = parse_err; + } + return false; + } + + int start_size = nodes->size(); + + for (const StringPiece &p : output_paths) { + uint64_t slash_bits; + std::string canonical = p.AsString(); + CanonicalizePath(&canonical, &slash_bits); + Node* new_node = state_->GetNode(canonical, slash_bits); + nodes->push_back(new_node); + } + *outputs_count = (int) nodes->size() - start_size; + + if (!g_keep_dynout) { + if (disk_interface_->RemoveFile(dynout_file) < 0) { + if (err != NULL) { + *err = std::string("deleting dynout: ") + strerror(errno) + std::string("\n"); + } + return false; + } + } + return true; +} diff --git a/src/build.h b/src/build.h index 9bb0c70b5c..d08710d5fd 100644 --- a/src/build.h +++ b/src/build.h @@ -233,6 +233,10 @@ struct Builder { const std::string& deps_prefix, std::vector* deps_nodes, std::string* err); + bool ExtractDynouts(Edge* edge, const std::string& dynout_file, + std::vector* nodes, int* outputs_count, + std::string* err); + /// Map of running edge to time the edge started running. typedef std::map RunningEdgeMap; RunningEdgeMap running_edges_; diff --git a/src/build_log.cc b/src/build_log.cc index 52c7c84f85..a4e43c1c8e 100644 --- a/src/build_log.cc +++ b/src/build_log.cc @@ -142,33 +142,36 @@ bool BuildLog::OpenForWrite(const string& path, const BuildLogUser& user, } bool BuildLog::RecordCommand(Edge* edge, int start_time, int end_time, - TimeStamp mtime) { + TimeStamp mtime, + const std::vector &extra_outputs) { string command = edge->EvaluateCommand(true); uint64_t command_hash = LogEntry::HashCommand(command); - for (vector::iterator out = edge->outputs_.begin(); - out != edge->outputs_.end(); ++out) { - const string& path = (*out)->path(); - Entries::iterator i = entries_.find(path); - LogEntry* log_entry; - if (i != entries_.end()) { - log_entry = i->second; - } else { - log_entry = new LogEntry(path); - entries_.insert(Entries::value_type(log_entry->output, log_entry)); - } - log_entry->command_hash = command_hash; - log_entry->start_time = start_time; - log_entry->end_time = end_time; - log_entry->mtime = mtime; - if (!OpenForWriteIfNeeded()) { - return false; - } - if (log_file_) { - if (!WriteEntry(log_file_, *log_entry)) + for (const vector outputs : { edge->outputs_, extra_outputs }) { + for (const Node *out : outputs) { + const string& path = out->path(); + Entries::iterator i = entries_.find(path); + LogEntry* log_entry; + if (i != entries_.end()) { + log_entry = i->second; + } else { + log_entry = new LogEntry(path); + entries_.insert(Entries::value_type(log_entry->output, log_entry)); + } + log_entry->command_hash = command_hash; + log_entry->start_time = start_time; + log_entry->end_time = end_time; + log_entry->mtime = mtime; + + if (!OpenForWriteIfNeeded()) { return false; - if (fflush(log_file_) != 0) { + } + if (log_file_) { + if (!WriteEntry(log_file_, *log_entry)) return false; + if (fflush(log_file_) != 0) { + return false; + } } } } diff --git a/src/build_log.h b/src/build_log.h index 1223c2a16a..4a84becbbb 100644 --- a/src/build_log.h +++ b/src/build_log.h @@ -25,6 +25,7 @@ struct DiskInterface; struct Edge; +struct Node; /// Can answer questions about the manifest for the BuildLog. struct BuildLogUser { @@ -49,7 +50,8 @@ struct BuildLog { bool OpenForWrite(const std::string& path, const BuildLogUser& user, std::string* err); bool RecordCommand(Edge* edge, int start_time, int end_time, - TimeStamp mtime = 0); + TimeStamp mtime = 0, + const std::vector& extra_outputs = {}); void Close(); /// Load the on-disk log. diff --git a/src/build_log_test.cc b/src/build_log_test.cc index 12c2dc742c..4aa4f3fe67 100644 --- a/src/build_log_test.cc +++ b/src/build_log_test.cc @@ -365,4 +365,29 @@ TEST_F(BuildLogRecompactTest, Recompact) { ASSERT_FALSE(log2.LookupByOutput("out2")); } +// Make sure the build log can record extra outputs not part of the edge (e.g. +// from a dynout file). +TEST_F(BuildLogTest, ExtraOutputs) { + AssertParse(&state_, +"build out: cat mid\n"); + + BuildLog log; + + // The build log must handle overlap between the extra outputs and the edge + // outputs, and only record each output once + Node *out_node = state_.LookupNode("out"); + ASSERT_TRUE(out_node); + Node *second_out = state_.GetNode("out.bis", 0); + std::vector extra_outputs = { out_node, second_out }; + + log.RecordCommand(state_.edges_[0], 15, 18, 0, extra_outputs); + + ASSERT_EQ(2u, log.entries().size()); + BuildLog::LogEntry* e1 = log.LookupByOutput("out.bis"); + ASSERT_TRUE(e1); + ASSERT_EQ(15, e1->start_time); + ASSERT_EQ("out.bis", e1->output); +} + + } // anonymous namespace diff --git a/src/build_test.cc b/src/build_test.cc index c84190a040..265c3c3a77 100644 --- a/src/build_test.cc +++ b/src/build_test.cc @@ -660,6 +660,18 @@ bool FakeCommandRunner::StartCommand(Edge* edge) { if (fs_->ReadFile(edge->inputs_[0]->path(), &content, &err) == DiskInterface::Okay) fs_->WriteFile(edge->outputs_[0]->path(), content); + } else if (edge->rule().name() == "cp-plus-bis") { + assert(!edge->inputs_.empty()); + assert(edge->outputs_.size() >= 1); + string content; + string err; + if (fs_->ReadFile(edge->inputs_[0]->path(), &content, &err) == + DiskInterface::Okay) { + fs_->Tick(); + fs_->WriteFile(edge->outputs_[0]->path() + ".bis", content); + fs_->Tick(); + fs_->WriteFile(edge->outputs_[0]->path(), content); + } } else if (edge->rule().name() == "touch-implicit-dep-out") { string dep = edge->GetBinding("test_dependency"); fs_->Tick(); @@ -4381,3 +4393,168 @@ TEST_F(BuildTest, ValidationWithCircularDependency) { EXPECT_FALSE(builder_.AddTarget("out", &err)); EXPECT_EQ("dependency cycle: validate -> validate_in -> validate", err); } + +TEST_F(BuildWithDepsLogTest, RebuildMissingDynamicOutputs) { + string err; + BuildLog build_log; + + { + FakeCommandRunner command_runner(&fs_); + State state; + DepsLog deps_log; + ASSERT_TRUE(deps_log.OpenForWrite(deps_log_file_.path(), &err)); + ASSERT_EQ("", err); + Builder builder(&state, config_, &build_log, &deps_log, &fs_, &status_, 0); + builder.command_runner_.reset(&command_runner); + + ASSERT_NO_FATAL_FAILURE( + AssertParse(&state, + "rule cp-plus-bis\n" + " command = cp $in $out && cp $in $out.bis\n" + " dynout = $out.dynout\n" + "build out: cp-plus-bis in\n")); + fs_.Tick(); + fs_.Create("in", ""); + EXPECT_TRUE(builder.AddTarget("out", &err)); + + fs_.Create("out.dynout", "out.bis\n"); + + EXPECT_TRUE(builder.Build(&err)); + ASSERT_EQ("", err); + ASSERT_EQ(1u, command_runner.commands_ran_.size()); + ASSERT_GT(fs_.Stat("out", &err), 0); + ASSERT_GT(fs_.Stat("out.bis", &err), 0); + // Make sure the dynout file has been removed after its + // information has been extracted in the deps log. + ASSERT_EQ(fs_.Stat("out.dynout", &err), 0); + + // all clean, no rebuild. + command_runner.commands_ran_.clear(); + state.Reset(); + EXPECT_TRUE(builder.AddTarget("out", &err)); + EXPECT_EQ("", err); + EXPECT_TRUE(builder.AlreadyUpToDate()); + + // Test dynamic output are rebuilt if they are deleted. + fs_.RemoveFile("out.bis"); + command_runner.commands_ran_.clear(); + state.Reset(); + + fs_.Create("out.dynout", "out.bis\n"); + EXPECT_TRUE(builder.AddTarget("out", &err)); + EXPECT_TRUE(builder.Build(&err)); + ASSERT_EQ("", err); + ASSERT_EQ(1u, command_runner.commands_ran_.size()); + + builder.command_runner_.release(); + deps_log.Close(); + } + + // Create a new state to make sure outputs are reset + { + FakeCommandRunner command_runner(&fs_); + State state; + DepsLog deps_log; + ASSERT_TRUE(deps_log.OpenForWrite(deps_log_file_.path(), &err)); + ASSERT_EQ("", err); + deps_log.Load(deps_log_file_.path(), &state, &err); + ASSERT_EQ("", err); + Builder builder(&state, config_, &build_log, &deps_log, &fs_, &status_, 0); + builder.command_runner_.reset(&command_runner); + + ASSERT_NO_FATAL_FAILURE( + AssertParse(&state, + "rule cp-plus-bis\n" + " command = cp $in $out && cp $in $out.bis\n" + " dynout = $out.dynout\n" + "build out: cp-plus-bis in\n")); + + // all clean, no rebuild. + command_runner.commands_ran_.clear(); + state.Reset(); + EXPECT_TRUE(builder.AddTarget("out", &err)); + EXPECT_EQ("", err); + EXPECT_TRUE(builder.AlreadyUpToDate()); + + // Test dynamic output are rebuilt if they are deleted + // after having been rebuilt in order to make sure + // when dynamic output information was already exist they + // keep being valid for the next build. + fs_.RemoveFile("out.bis"); + command_runner.commands_ran_.clear(); + state.Reset(); + + // Recreate the dynout file because it is not created by the edge + fs_.Create("out.dynout", "out.bis\n"); + EXPECT_TRUE(builder.AddTarget("out", &err)); + EXPECT_TRUE(builder.Build(&err)); + ASSERT_EQ("", err); + ASSERT_EQ(1u, command_runner.commands_ran_.size()); + + builder.command_runner_.release(); + deps_log.Close(); + } +} + +TEST_F(BuildWithDepsLogTest, RebuildMissingDynamicOutputsWithRestat) { + string err; + + FakeCommandRunner command_runner(&fs_); + BuildLog build_log; + State state; + DepsLog deps_log; + ASSERT_TRUE(deps_log.OpenForWrite(deps_log_file_.path(), &err)); + ASSERT_EQ("", err); + Builder builder(&state, config_, &build_log, &deps_log, &fs_, &status_, 0); + builder.command_runner_.reset(&command_runner); + + ASSERT_NO_FATAL_FAILURE( + AssertParse(&state, + "rule cp-plus-bis\n" + " command = cp $in $out && cp $in $out.bis\n" + " dynout = $out.dynout\n" + " restat = 1\n" + "build out: cp-plus-bis in\n")); + + fs_.Tick(); + fs_.Create("in", ""); + fs_.Tick(); + fs_.Create("out.dynout", "out.bis\n"); + fs_.Tick(); + + EXPECT_TRUE(builder.AddTarget("out", &err)); + EXPECT_TRUE(builder.Build(&err)); + ASSERT_EQ("", err); + ASSERT_EQ(1u, command_runner.commands_ran_.size()); + ASSERT_GT(fs_.Stat("out", &err), 0); + ASSERT_GT(fs_.Stat("out.bis", &err), 0); + // Make sure the dynout file has been removed after its + // information has been extracted in the deps log. + ASSERT_EQ(fs_.Stat("out.dynout", &err), 0); + + // all clean, no rebuild. + command_runner.commands_ran_.clear(); + state.Reset(); + EXPECT_TRUE(builder.AddTarget("out", &err)); + EXPECT_EQ("", err); + EXPECT_TRUE(builder.AlreadyUpToDate()); + + fs_.RemoveFile("out.bis"); + command_runner.commands_ran_.clear(); + state.Reset(); + + // Recreate the dynout file because it is not created by the edge + fs_.Create("out.dynout", "out.bis\n"); + EXPECT_TRUE(builder.AddTarget("out", &err)); + EXPECT_TRUE(builder.Build(&err)); + ASSERT_EQ("", err); + ASSERT_EQ(1u, command_runner.commands_ran_.size()); + + // Make sure the dynout file has been removed after its + // information has been extracted in the deps log. + ASSERT_EQ(fs_.Stat("out.dynout", &err), 0); + + builder.command_runner_.release(); + + deps_log.Close(); +} diff --git a/src/clean.cc b/src/clean.cc index ceffe64027..93ee84ccc1 100644 --- a/src/clean.cc +++ b/src/clean.cc @@ -26,12 +26,14 @@ using namespace std; Cleaner::Cleaner(State* state, const BuildConfig& config, - DiskInterface* disk_interface) + DiskInterface* disk_interface, + DepsLog* deps_log) : state_(state), config_(config), dyndep_loader_(state, disk_interface), cleaned_files_count_(0), disk_interface_(disk_interface), + deps_log_(deps_log), status_(0) { } @@ -79,6 +81,10 @@ void Cleaner::RemoveEdgeFiles(Edge* edge) { if (!depfile.empty()) Remove(depfile); + string dynout = edge->GetUnescapedDynout(); + if (!dynout.empty()) + Remove(dynout); + string rspfile = edge->GetUnescapedRspfile(); if (!rspfile.empty()) Remove(rspfile); @@ -105,6 +111,7 @@ int Cleaner::CleanAll(bool generator) { Reset(); PrintHeader(); LoadDyndeps(); + LoadDynamicOutputs(); for (vector::iterator e = state_->edges_.begin(); e != state_->edges_.end(); ++e) { // Do not try to remove phony targets @@ -174,6 +181,7 @@ int Cleaner::CleanTarget(Node* target) { Reset(); PrintHeader(); LoadDyndeps(); + LoadDynamicOutputs(); DoCleanTarget(target); PrintFooter(); return status_; @@ -197,6 +205,7 @@ int Cleaner::CleanTargets(int target_count, char* targets[]) { Reset(); PrintHeader(); LoadDyndeps(); + LoadDynamicOutputs(); for (int i = 0; i < target_count; ++i) { string target_name = targets[i]; if (target_name.empty()) { @@ -241,6 +250,7 @@ int Cleaner::CleanRule(const Rule* rule) { Reset(); PrintHeader(); LoadDyndeps(); + LoadDynamicOutputs(); DoCleanRule(rule); PrintFooter(); return status_; @@ -266,6 +276,7 @@ int Cleaner::CleanRules(int rule_count, char* rules[]) { Reset(); PrintHeader(); LoadDyndeps(); + LoadDynamicOutputs(); for (int i = 0; i < rule_count; ++i) { const char* rule_name = rules[i]; const Rule* rule = state_->bindings_.LookupRule(rule_name); @@ -302,3 +313,19 @@ void Cleaner::LoadDyndeps() { } } } + +void Cleaner::LoadDynamicOutputs() { + std::string err; + // Load dynamic outputs which may exist in the deps log + DepfileParserOptions depfileOptions; + ImplicitDepLoader implicit_dep_loader(state_, deps_log_, disk_interface_, + &depfileOptions, nullptr); + for (vector::iterator e = state_->edges_.begin(); + e != state_->edges_.end(); ++e) { + string dynout = (*e)->GetUnescapedDynout(); + + if (!dynout.empty()) { + implicit_dep_loader.LoadImplicitOutputs(*e, &err); + } + } +} diff --git a/src/clean.h b/src/clean.h index cf3f1c3f3d..fe04f576cf 100644 --- a/src/clean.h +++ b/src/clean.h @@ -31,7 +31,8 @@ struct Cleaner { /// Build a cleaner object with the given @a disk_interface Cleaner(State* state, const BuildConfig& config, - DiskInterface* disk_interface); + DiskInterface* disk_interface, + DepsLog* deps_log); /// Clean the given @a target and all the file built for it. /// @return non-zero if an error occurs. @@ -98,6 +99,9 @@ struct Cleaner { /// Load dependencies from dyndep bindings. void LoadDyndeps(); + /// Load dynamic outputs from deps log. + void LoadDynamicOutputs(); + State* state_; const BuildConfig& config_; DyndepLoader dyndep_loader_; @@ -105,6 +109,7 @@ struct Cleaner { std::set cleaned_; int cleaned_files_count_; DiskInterface* disk_interface_; + DepsLog* deps_log_; int status_; }; diff --git a/src/clean_test.cc b/src/clean_test.cc index e99909c0d0..360bb86236 100644 --- a/src/clean_test.cc +++ b/src/clean_test.cc @@ -14,6 +14,7 @@ #include "clean.h" #include "build.h" +#include "deps_log.h" #include "util.h" #include "test.h" @@ -47,7 +48,8 @@ TEST_F(CleanTest, CleanAll) { fs_.Create("in2", ""); fs_.Create("out2", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); EXPECT_EQ(0, cleaner.CleanAll()); @@ -79,7 +81,8 @@ TEST_F(CleanTest, CleanAllDryRun) { fs_.Create("out2", ""); config_.dry_run = true; - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); EXPECT_EQ(0, cleaner.CleanAll()); @@ -110,7 +113,8 @@ TEST_F(CleanTest, CleanTarget) { fs_.Create("in2", ""); fs_.Create("out2", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); ASSERT_EQ(0, cleaner.CleanTarget("out1")); @@ -142,7 +146,8 @@ TEST_F(CleanTest, CleanTargetDryRun) { fs_.Create("out2", ""); config_.dry_run = true; - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); ASSERT_EQ(0, cleaner.CleanTarget("out1")); @@ -175,7 +180,8 @@ TEST_F(CleanTest, CleanRule) { fs_.Create("in2", ""); fs_.Create("out2", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); ASSERT_EQ(0, cleaner.CleanRule("cat_e")); @@ -209,7 +215,8 @@ TEST_F(CleanTest, CleanRuleDryRun) { fs_.Create("out2", ""); config_.dry_run = true; - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); ASSERT_EQ(0, cleaner.CleanRule("cat_e")); @@ -239,7 +246,8 @@ TEST_F(CleanTest, CleanRuleGenerator) { fs_.Create("out1", ""); fs_.Create("out2", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner.CleanAll()); EXPECT_EQ(1, cleaner.cleaned_files_count()); EXPECT_EQ(1u, fs_.files_removed_.size()); @@ -260,7 +268,8 @@ TEST_F(CleanTest, CleanDepFile) { fs_.Create("out1", ""); fs_.Create("out1.d", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner.CleanAll()); EXPECT_EQ(2, cleaner.cleaned_files_count()); EXPECT_EQ(2u, fs_.files_removed_.size()); @@ -275,7 +284,8 @@ TEST_F(CleanTest, CleanDepFileOnCleanTarget) { fs_.Create("out1", ""); fs_.Create("out1.d", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner.CleanTarget("out1")); EXPECT_EQ(2, cleaner.cleaned_files_count()); EXPECT_EQ(2u, fs_.files_removed_.size()); @@ -290,7 +300,8 @@ TEST_F(CleanTest, CleanDepFileOnCleanRule) { fs_.Create("out1", ""); fs_.Create("out1.d", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner.CleanRule("cc")); EXPECT_EQ(2, cleaner.cleaned_files_count()); EXPECT_EQ(2u, fs_.files_removed_.size()); @@ -311,7 +322,8 @@ TEST_F(CleanTest, CleanDyndep) { fs_.Create("out", ""); fs_.Create("out.imp", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); EXPECT_EQ(0, cleaner.CleanAll()); @@ -333,7 +345,8 @@ TEST_F(CleanTest, CleanDyndepMissing) { fs_.Create("out", ""); fs_.Create("out.imp", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); EXPECT_EQ(0, cleaner.CleanAll()); @@ -356,7 +369,8 @@ TEST_F(CleanTest, CleanRspFile) { fs_.Create("out1", ""); fs_.Create("cc1.rsp", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner.CleanAll()); EXPECT_EQ(2, cleaner.cleaned_files_count()); EXPECT_EQ(2u, fs_.files_removed_.size()); @@ -382,7 +396,8 @@ TEST_F(CleanTest, CleanRsp) { fs_.Create("in2", ""); fs_.Create("out2", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); ASSERT_EQ(0, cleaner.cleaned_files_count()); ASSERT_EQ(0, cleaner.CleanTarget("out1")); EXPECT_EQ(2, cleaner.cleaned_files_count()); @@ -407,7 +422,8 @@ TEST_F(CleanTest, CleanFailure) { ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, "build dir: cat src1\n")); fs_.MakeDir("dir"); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); EXPECT_NE(0, cleaner.CleanAll()); } @@ -423,7 +439,8 @@ TEST_F(CleanTest, CleanPhony) { fs_.Create("t2", ""); // Check that CleanAll does not remove "phony". - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner.CleanAll()); EXPECT_EQ(2, cleaner.cleaned_files_count()); EXPECT_LT(0, fs_.Stat("phony", &err)); @@ -454,7 +471,8 @@ TEST_F(CleanTest, CleanDepFileAndRspFileWithSpaces) { fs_.Create("out 1.d", ""); fs_.Create("out 2.rsp", ""); - Cleaner cleaner(&state_, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner(&state_, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner.CleanAll()); EXPECT_EQ(4, cleaner.cleaned_files_count()); EXPECT_EQ(4u, fs_.files_removed_.size()); @@ -509,7 +527,8 @@ TEST_F(CleanDeadTest, CleanDead) { ASSERT_TRUE(log2.LookupByOutput("out2")); // First use the manifest that describe how to build out1. - Cleaner cleaner1(&state, config_, &fs_); + DepsLog deps_log; + Cleaner cleaner1(&state, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner1.CleanDead(log2.entries())); EXPECT_EQ(0, cleaner1.cleaned_files_count()); EXPECT_EQ(0u, fs_.files_removed_.size()); @@ -518,7 +537,7 @@ TEST_F(CleanDeadTest, CleanDead) { EXPECT_NE(0, fs_.Stat("out2", &err)); // Then use the manifest that does not build out1 anymore. - Cleaner cleaner2(&state_, config_, &fs_); + Cleaner cleaner2(&state_, config_, &fs_, &deps_log); EXPECT_EQ(0, cleaner2.CleanDead(log2.entries())); EXPECT_EQ(1, cleaner2.cleaned_files_count()); EXPECT_EQ(1u, fs_.files_removed_.size()); @@ -572,7 +591,8 @@ TEST_F(CleanDeadTest, CleanDeadPreservesInputs) { ASSERT_TRUE(log2.LookupByOutput("out2")); // First use the manifest that describe how to build out1. - Cleaner cleaner1(&state, config_, &fs_); + DepsLog deps_log1; + Cleaner cleaner1(&state, config_, &fs_, &deps_log1); EXPECT_EQ(0, cleaner1.CleanDead(log2.entries())); EXPECT_EQ(0, cleaner1.cleaned_files_count()); EXPECT_EQ(0u, fs_.files_removed_.size()); @@ -581,7 +601,8 @@ TEST_F(CleanDeadTest, CleanDeadPreservesInputs) { EXPECT_NE(0, fs_.Stat("out2", &err)); // Then use the manifest that does not build out1 anymore. - Cleaner cleaner2(&state_, config_, &fs_); + DepsLog deps_log2; + Cleaner cleaner2(&state_, config_, &fs_, &deps_log2); EXPECT_EQ(0, cleaner2.CleanDead(log2.entries())); EXPECT_EQ(0, cleaner2.cleaned_files_count()); EXPECT_EQ(0u, fs_.files_removed_.size()); @@ -598,4 +619,85 @@ TEST_F(CleanDeadTest, CleanDeadPreservesInputs) { EXPECT_NE(0, fs_.Stat("out2", &err)); log2.Close(); } + +TEST_F(CleanTest, CleanDynamicOutputs) { + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, +"rule cp-plus-bis\n" +" command = cp $in $out && cp $in $out.bis\n" +" dynout = $out.dynout\n" +"build out: cp-plus-bis in\n" +)); + fs_.Create("out", ""); + fs_.Create("out.bis", ""); + fs_.Create("out.dynout", ""); + + string err; + DepsLog deps_log; + ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err)); + ASSERT_EQ("", err); + Node* out = state_.LookupNode("out"); + std::vector nodes; + Node* out_bis = state_.GetNode("out.bis", 0); + nodes.push_back(out_bis); + deps_log.RecordDeps(out, 0, nodes, 1); + + Cleaner cleaner(&state_, config_, &fs_, &deps_log); + EXPECT_EQ(0, cleaner.CleanAll()); + EXPECT_EQ(3, cleaner.cleaned_files_count()); + EXPECT_EQ(3u, fs_.files_removed_.size()); + + EXPECT_EQ(0, fs_.Stat("out", &err)); + EXPECT_EQ(0, fs_.Stat("out.bis", &err)); + EXPECT_EQ(0, fs_.Stat("out.dynout", &err)); + + deps_log.Close(); + RealDiskInterface disk_interface; + disk_interface.RemoveFile("ninja_deps"); +} + +TEST_F(CleanTest, CleanDynamicOutputForDirtyNode) { + // Even if the node is dirty, the cleaner should use the dynamic outputs from + // the deps log. It should not attempt to build the node to update the dynamic + // output set. + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, +"rule cp-plus-bis\n" +" command = cp $in $out && cp $in $out.bis\n" +" dynout = $out.dynout\n" +"build out: cp-plus-bis in\n" +)); + fs_.Create("out", ""); + fs_.Create("out.bis", ""); + + string err; + Node* out = state_.LookupNode("out"); + ASSERT_TRUE(out->StatIfNecessary(&fs_, &err)); + ASSERT_EQ("", err); + + DepsLog deps_log; + ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err)); + ASSERT_EQ("", err); + std::vector nodes; + Node* out_bis = state_.GetNode("out.bis", 0); + nodes.push_back(out_bis); + deps_log.RecordDeps(out, out->mtime(), nodes, 1); + + fs_.Tick(); + fs_.Create("in", ""); + Node* in = state_.LookupNode("in"); + ASSERT_TRUE(in->StatIfNecessary(&fs_, &err)); + ASSERT_EQ("", err); + + Cleaner cleaner(&state_, config_, &fs_, &deps_log); + EXPECT_EQ(0, cleaner.CleanAll()); + EXPECT_EQ(2, cleaner.cleaned_files_count()); + EXPECT_EQ(2u, fs_.files_removed_.size()); + + EXPECT_EQ(0, fs_.Stat("out", &err)); + EXPECT_EQ(0, fs_.Stat("out.bis", &err)); + + deps_log.Close(); + RealDiskInterface disk_interface; + disk_interface.RemoveFile("ninja_deps"); +} + } // anonymous namespace diff --git a/src/debug_flags.cc b/src/debug_flags.cc index c83abb4093..5ea17a71df 100644 --- a/src/debug_flags.cc +++ b/src/debug_flags.cc @@ -23,6 +23,8 @@ bool g_explaining = false; bool g_keep_depfile = false; +bool g_keep_dynout = false; + bool g_keep_rsp = false; bool g_experimental_statcache = true; diff --git a/src/debug_flags.h b/src/debug_flags.h index fe73a5202c..fa432f1584 100644 --- a/src/debug_flags.h +++ b/src/debug_flags.h @@ -24,6 +24,8 @@ extern bool g_explaining; extern bool g_keep_depfile; +extern bool g_keep_dynout; + extern bool g_keep_rsp; extern bool g_experimental_statcache; diff --git a/src/deps_log.cc b/src/deps_log.cc index af1b4f1300..e3de7b6525 100644 --- a/src/deps_log.cc +++ b/src/deps_log.cc @@ -373,13 +373,14 @@ bool DepsLog::Recompact(const string& path, string* err) { } bool DepsLog::IsDepsEntryLiveFor(const Node* node) { - // Skip entries that don't have in-edges or whose edges don't have a - // "deps" attribute. They were in the deps log from previous builds, but - // the the files they were for were removed from the build and their deps - // entries are no longer needed. + // Skip entries that don't have in-edges or whose edges don't have a "deps" or + // "dynout" attribute. They were in the deps log from previous builds, but the + // the files they were for were removed from the build and their deps entries + // are no longer needed. // (Without the check for "deps", a chain of two or more nodes that each // had deps wouldn't be collected in a single recompaction.) - return node->in_edge() && !node->in_edge()->GetBinding("deps").empty(); + return node->in_edge() && (!node->in_edge()->GetBinding("deps").empty() || + !node->in_edge()->GetBinding("dynout").empty()); } bool DepsLog::UpdateDeps(int out_id, Deps* deps) { diff --git a/src/eval_env.cc b/src/eval_env.cc index 796a3264d1..255b236495 100644 --- a/src/eval_env.cc +++ b/src/eval_env.cc @@ -67,6 +67,7 @@ const EvalString* Rule::GetBinding(const string& key) const { bool Rule::IsReservedBinding(const string& var) { return var == "command" || var == "depfile" || + var == "dynout" || var == "dyndep" || var == "description" || var == "deps" || diff --git a/src/graph.cc b/src/graph.cc index 76ecfb0a6e..3f1857aef7 100644 --- a/src/graph.cc +++ b/src/graph.cc @@ -22,6 +22,7 @@ #include "build_log.h" #include "debug_flags.h" #include "depfile_parser.h" +#include "dynout_parser.h" #include "deps_log.h" #include "disk_interface.h" #include "manifest_parser.h" @@ -148,7 +149,7 @@ bool DependencyScan::RecomputeNodeDirty(Node* node, std::vector* stack, if (!edge->deps_loaded_) { // This is our first encounter with this edge. Load discovered deps. edge->deps_loaded_ = true; - if (!dep_loader_.LoadDeps(edge, err)) { + if (!dep_loader_.LoadDeps(edge, err) || !dep_loader_.LoadImplicitOutputs(edge, err)) { if (!err->empty()) return false; // Failed to load dependency info: rebuild to regenerate it. @@ -542,6 +543,11 @@ string Edge::GetUnescapedDepfile() const { return env.LookupVariable("depfile"); } +string Edge::GetUnescapedDynout() const { + EdgeEnv env(this, EdgeEnv::kDoNotEscape); + return env.LookupVariable("dynout"); +} + string Edge::GetUnescapedDyndep() const { EdgeEnv env(this, EdgeEnv::kDoNotEscape); return env.LookupVariable("dyndep"); @@ -648,6 +654,21 @@ bool ImplicitDepLoader::LoadDeps(Edge* edge, string* err) { return true; } +bool ImplicitDepLoader::LoadImplicitOutputs(Edge* edge, string* err) { + string dynout = edge->GetUnescapedDynout(); + if (!dynout.empty()) { + if (LoadOutputsFromLog(edge, err)) + return true; + if (err != NULL && !err->empty()) + return false; + + return LoadDynoutFile(edge, dynout, err); + } + + // No outputs to load. + return true; +} + struct matches { explicit matches(std::vector::iterator i) : i_(i) {} @@ -772,6 +793,127 @@ bool ImplicitDepLoader::LoadDepsFromLog(Edge* edge, string* err) { return true; } +bool ImplicitDepLoader::LoadDynoutFile(Edge* edge, const std::string& path, + std::string* err) { + METRIC_RECORD("dynout load"); + // Read depfile content. Treat a missing depfile as empty. + string content; + switch (disk_interface_->ReadFile(path, &content, err)) { + case DiskInterface::Okay: + break; + case DiskInterface::NotFound: + err->clear(); + break; + case DiskInterface::OtherError: + *err = "loading '" + path + "': " + *err; + return false; + } + // On a missing depfile: return false and empty *err. + Node* first_output = edge->outputs_[0]; + if (content.empty()) { + explanations_.Record(first_output, "dynout '%s' is missing", path.c_str()); + return false; + } + + std::string dynout_err; + std::vector output_paths; + if (!DynoutParser::Parse(content, output_paths, &dynout_err)) { + *err = path + ": " + dynout_err; + return false; + } + + std::vector new_implicit_outputs; + new_implicit_outputs.reserve(output_paths.size()); + for (const StringPiece &p : output_paths) { + uint64_t slash_bits; + std::string canonical = p.AsString(); + CanonicalizePath(&canonical, &slash_bits); + Node* new_node = state_->GetNode(canonical, slash_bits); + bool exists = std::find(edge->outputs_.begin(), edge->outputs_.end(), + new_node) != edge->outputs_.end(); + if (!exists) { + std::string stat_err; + if (!new_node->StatIfNecessary(disk_interface_, &stat_err)) { + *err = "stat '" + new_node->path() + "': " + stat_err; + return false; + } + new_implicit_outputs.push_back(new_node); + } + } + + // Add the dyndep-discovered outputs to the edge. + edge->outputs_.insert(edge->outputs_.end(), + new_implicit_outputs.begin(), + new_implicit_outputs.end()); + edge->implicit_outs_ += new_implicit_outputs.size(); + + // Add this edge as incoming to each new output. + for (Node* output : new_implicit_outputs) { + if (output->in_edge()) { + if (err != NULL) { + *err = "multiple rules generate " + output->path(); + } + return false; + } + output->set_in_edge(edge); + } + + return true; +} + +bool ImplicitDepLoader::LoadOutputsFromLog(Edge* edge, string* err) { + std::vector implicit_outputs; + + for (Node *output : edge->outputs_) { + DepsLog::Deps* deps = deps_log_ ? deps_log_->GetDeps(output) : NULL; + if (!deps) { + explanations_.Record(output, "outputs for '%s' are missing", output->path().c_str()); + return false; + } + + // Deps are invalid if the output is newer than the deps. + if (output->mtime() > deps->mtime) { + explanations_.Record(output, + "stored outputs info out of date for '%s' (%" PRId64 + " vs %" PRId64 ")", + output->path().c_str(), deps->mtime, output->mtime()); + return false; + } + + for (int i = deps->node_count - deps->outputs_count; i < deps->node_count; i++) { + Node* new_node = deps->nodes[i]; + bool exists = std::find(edge->outputs_.begin(), edge->outputs_.end(), + new_node) != edge->outputs_.end(); + if (!exists) { + std::string stat_err; + if (!new_node->StatIfNecessary(disk_interface_, &stat_err)) { + *err = "stat '" + new_node->path() + "': " + stat_err; + return false; + } + implicit_outputs.push_back(new_node); + } + } + } + + // Add the dyndep-discovered outputs to the edge. + edge->outputs_.insert(edge->outputs_.end(), implicit_outputs.begin(), + implicit_outputs.end()); + edge->implicit_outs_ += implicit_outputs.size(); + + // Add this edge as incoming to each new output. + for (Node *output : implicit_outputs) { + if (output->in_edge()) { + if (err != NULL) { + *err = "multiple rules generate " + output->path(); + } + return false; + } + output->set_in_edge(edge); + } + + return true; +} + vector::iterator ImplicitDepLoader::PreallocateSpace(Edge* edge, int count) { edge->inputs_.insert(edge->inputs_.end() - edge->order_only_deps_, diff --git a/src/graph.h b/src/graph.h index 314c44296a..7227ecc41d 100644 --- a/src/graph.h +++ b/src/graph.h @@ -194,6 +194,8 @@ struct Edge { /// Like GetBinding("depfile"), but without shell escaping. std::string GetUnescapedDepfile() const; + /// Like GetBinding("dynout"), but without shell escaping. + std::string GetUnescapedDynout() const; /// Like GetBinding("dyndep"), but without shell escaping. std::string GetUnescapedDyndep() const; /// Like GetBinding("rspfile"), but without shell escaping. @@ -279,8 +281,8 @@ struct EdgeCmp { typedef std::set EdgeSet; -/// ImplicitDepLoader loads implicit dependencies, as referenced via the -/// "depfile" attribute in build files. +/// ImplicitDepLoader loads implicit dependencies and outputs, as referenced via +/// the "depfile" and "dynout" attributes in build files. struct ImplicitDepLoader { ImplicitDepLoader(State* state, DepsLog* deps_log, DiskInterface* disk_interface, @@ -295,6 +297,11 @@ struct ImplicitDepLoader { // or out of date). bool LoadDeps(Edge* edge, std::string* err); + /// Load implicit outputs for \a edge. + /// @return false on error (without filling \a err if info is just missing + // or out of date). + bool LoadImplicitOutputs(Edge* edge, std::string* err); + DepsLog* deps_log() const { return deps_log_; } @@ -314,6 +321,14 @@ struct ImplicitDepLoader { /// @return false on error (without filling \a err if info is just missing). bool LoadDepsFromLog(Edge* edge, std::string* err); + /// Load implicit outputs for \a edge from a dynout attribute. + /// @return false on error (without filling \a err if info is just missing). + bool LoadDynoutFile(Edge* edge, const std::string &path, std::string* err); + + /// Load implicit outputs for \a edge from the DepsLog. + /// @return false on error (without filling \a err if info is just missing). + bool LoadOutputsFromLog(Edge* edge, std::string* err); + /// Preallocate \a count spaces in the input array on \a edge, returning /// an iterator pointing at the first new space. std::vector::iterator PreallocateSpace(Edge* edge, int count); diff --git a/src/graph_test.cc b/src/graph_test.cc index f909b906fd..64cba63764 100644 --- a/src/graph_test.cc +++ b/src/graph_test.cc @@ -305,6 +305,91 @@ TEST_F(GraphTest, DepfileRemoved) { EXPECT_TRUE(GetNode("out.o")->dirty()); } +TEST_F(GraphTest, DynoutFileRemoved) { + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, +"rule catdynout\n" +" dynout = $out.dynout\n" +" command = cat $in > $out\n" +"build ./out.o: catdynout ./foo.cc\n")); + fs_.Create("foo.cc", ""); + fs_.Tick(); + fs_.Create("out.o.dynout", "foo.bis"); + fs_.Create("out.o", ""); + fs_.Create("foo.bis", ""); + + string err; + EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err)); + ASSERT_EQ("", err); + EXPECT_FALSE(GetNode("out.o")->dirty()); + + state_.Reset(); + fs_.RemoveFile("out.o.dynout"); + EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err)); + ASSERT_EQ("", err); + EXPECT_TRUE(GetNode("out.o")->dirty()); +} + +TEST_F(GraphTest, DynamicOutputMissing) { + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, +"rule catdynout\n" +" dynout = $out.dynout\n" +" command = cat $in > $out\n" +"build ./out.o: catdynout ./foo.cc\n")); + fs_.Create("foo.cc", ""); + fs_.Tick(); + fs_.Create("out.o.dynout", "foo.bis"); + fs_.Create("out.o", ""); + + string err; + EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err)); + ASSERT_EQ("", err); + EXPECT_TRUE(GetNode("out.o")->dirty()); + + state_.Reset(); + fs_.Create("foo.bis", ""); + EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err)); + ASSERT_EQ("", err); + EXPECT_FALSE(GetNode("out.o")->dirty()); +} + +// Outputs specified both in the build statement and the dynout file should +// appear only once in the node outputs +TEST_F(GraphTest, DynamicOutputOverlap) { + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, +"rule catdynout\n" +" dynout = $out.dynout\n" +" command = cat $in > $out\n" +"build ./out.o: catdynout ./foo.cc\n")); + fs_.Create("foo.cc", ""); + fs_.Tick(); + fs_.Create("out.o.dynout", "out.o\nfoo.bis"); + fs_.Create("out.o", ""); + + string err; + EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err)); + ASSERT_EQ("", err); + + Node* node = GetNode("foo.cc"); + EXPECT_EQ(1, node->out_edges().size()); + EXPECT_EQ(2, node->out_edges()[0]->outputs_.size()); + EXPECT_EQ("out.o", node->out_edges()[0]->outputs_[0]->path()); + EXPECT_EQ("foo.bis", node->out_edges()[0]->outputs_[1]->path()); +} + +TEST_F(GraphTest, CycleWithLengthZeroFromDynout) { + AssertParse(&state_, +"rule dynoutrule\n" +" dynout = dynout.dynout\n" +" command = unused\n" +"build a b: dynoutrule dep\n" + ); + fs_.Create("dynout.dynout", "dep"); + + string err; + EXPECT_FALSE(scan_.RecomputeDirty(GetNode("b"), NULL, &err)); + ASSERT_EQ("dependency cycle: dep -> dep", err); +} + // Check that rule-level variables are in scope for eval. TEST_F(GraphTest, RuleVariablesInScope) { ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, diff --git a/src/manifest_parser_test.cc b/src/manifest_parser_test.cc index c5a1fe8fd2..96d8893668 100644 --- a/src/manifest_parser_test.cc +++ b/src/manifest_parser_test.cc @@ -64,6 +64,7 @@ TEST_F(ParserTest, RuleAttributes) { " command = a\n" " depfile = a\n" " deps = a\n" +" dynout = a\n" " description = a\n" " generator = a\n" " restat = a\n" diff --git a/src/ninja.cc b/src/ninja.cc index 2902359f15..1b26cb059f 100644 --- a/src/ninja.cc +++ b/src/ninja.cc @@ -865,7 +865,7 @@ int NinjaMain::ToolClean(const Options* options, int argc, char* argv[]) { return 1; } - Cleaner cleaner(&state_, config_, &disk_interface_); + Cleaner cleaner(&state_, config_, &disk_interface_, &deps_log_); if (argc >= 1) { if (clean_rules) return cleaner.CleanRules(argc, argv); @@ -877,7 +877,7 @@ int NinjaMain::ToolClean(const Options* options, int argc, char* argv[]) { } int NinjaMain::ToolCleanDead(const Options* options, int argc, char* argv[]) { - Cleaner cleaner(&state_, config_, &disk_interface_); + Cleaner cleaner(&state_, config_, &disk_interface_, &deps_log_); return cleaner.CleanDead(build_log_.entries()); } @@ -1111,7 +1111,7 @@ const Tool* ChooseTool(const string& tool_name) { Tool::RUN_AFTER_FLAGS, &NinjaMain::ToolMSVC }, #endif { "clean", "clean built files", - Tool::RUN_AFTER_LOAD, &NinjaMain::ToolClean }, + Tool::RUN_AFTER_LOGS, &NinjaMain::ToolClean }, { "commands", "list all commands required to rebuild given targets", Tool::RUN_AFTER_LOAD, &NinjaMain::ToolCommands }, { "inputs", "list all inputs required to rebuild given targets", @@ -1180,6 +1180,7 @@ bool DebugEnable(const string& name) { " stats print operation counts/timing info\n" " explain explain what caused a command to execute\n" " keepdepfile don't delete depfiles after they're read by ninja\n" +" keepdynout don't delete dynout files after they're read by ninja\n" " keeprsp don't delete @response files on success\n" #ifdef _WIN32 " nostatcache don't batch stat() calls per directory and cache them\n" @@ -1195,6 +1196,9 @@ bool DebugEnable(const string& name) { } else if (name == "keepdepfile") { g_keep_depfile = true; return true; + } else if (name == "keepdynout") { + g_keep_dynout = true; + return true; } else if (name == "keeprsp") { g_keep_rsp = true; return true; @@ -1204,7 +1208,7 @@ bool DebugEnable(const string& name) { } else { const char* suggestion = SpellcheckString(name.c_str(), - "stats", "explain", "keepdepfile", "keeprsp", + "stats", "explain", "keepdepfile", "keepdynout", "keeprsp", "nostatcache", NULL); if (suggestion) { Error("unknown debug setting '%s', did you mean '%s'?", From fb6170d633b702486f3ca5c592ff86c79fc89715 Mon Sep 17 00:00:00 2001 From: Marc Delorme Date: Thu, 15 Apr 2021 21:15:28 +0900 Subject: [PATCH 4/4] Add tools: 'outputs' and 'dynouts' 'outputs' lists all the outputs generated by the graph, including dynamic outputs. 'dynouts' lists all dynamic outputs stored in the deps log. Co-authored-by: Hampus Adolfsson --- doc/manual.asciidoc | 6 +++ src/ninja.cc | 89 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/doc/manual.asciidoc b/doc/manual.asciidoc index fc52647942..b84c6b7a13 100644 --- a/doc/manual.asciidoc +++ b/doc/manual.asciidoc @@ -292,6 +292,9 @@ _Available since Ninja 1.2._ `deps`:: show all dependencies stored in the `.ninja_deps` file. When given a target, show just the target's dependencies. _Available since Ninja 1.4._ +`dynouts`:: show all dynamic outputs stored in the `.ninja_deps` file. When +given a target, show just the target's outputs. + `missingdeps`:: given a list of targets, look for targets that depend on a generated file, but do not have a properly (possibly transitive) dependency on the generator. Such targets may cause build flakiness on clean builds. @@ -307,6 +310,9 @@ each of them with a missing include error or equivalent pointing to the generated file. _Available since Ninja 1.11._ +`outputs`:: list all outputs of the build graph. This includes any dynamic +outputs stored in the deps log. + `recompact`:: recompact the `.ninja_deps` file. _Available since Ninja 1.4._ `restat`:: updates all recorded file modification timestamps in the `.ninja_log` diff --git a/src/ninja.cc b/src/ninja.cc index 1b26cb059f..a1dbc38049 100644 --- a/src/ninja.cc +++ b/src/ninja.cc @@ -125,6 +125,8 @@ struct NinjaMain : public BuildLogUser { int ToolBrowse(const Options* options, int argc, char* argv[]); int ToolMSVC(const Options* options, int argc, char* argv[]); int ToolTargets(const Options* options, int argc, char* argv[]); + int ToolDynOuts(const Options* options, int argc, char* argv[]); + int ToolOutputs(const Options* options, int argc, char* argv[]); int ToolCommands(const Options* options, int argc, char* argv[]); int ToolInputs(const Options* options, int argc, char* argv[]); int ToolClean(const Options* options, int argc, char* argv[]); @@ -570,10 +572,11 @@ int NinjaMain::ToolDeps(const Options* options, int argc, char** argv) { TimeStamp mtime = disk_interface.Stat((*it)->path(), &err); if (mtime == -1) Error("%s", err.c_str()); // Log and ignore Stat() errors; + int deps_count = deps->node_count - deps->outputs_count; printf("%s: #deps %d, deps mtime %" PRId64 " (%s)\n", - (*it)->path().c_str(), deps->node_count, deps->mtime, + (*it)->path().c_str(), deps_count, deps->mtime, (!mtime || mtime > deps->mtime ? "STALE":"VALID")); - for (int i = 0; i < deps->node_count; ++i) + for (int i = 0; i < deps_count; ++i) printf(" %s\n", deps->nodes[i]->path().c_str()); printf("\n"); } @@ -601,6 +604,84 @@ int NinjaMain::ToolMissingDeps(const Options* options, int argc, char** argv) { return 0; } +int NinjaMain::ToolDynOuts(const Options*, int argc, char** argv) { + vector nodes; + if (argc == 0) { + for (vector::const_iterator ni = deps_log_.nodes().begin(); + ni != deps_log_.nodes().end(); ++ni) { + if (deps_log_.IsDepsEntryLiveFor(*ni)) + nodes.push_back(*ni); + } + } else { + string err; + if (!CollectTargetsFromArgs(argc, argv, &nodes, &err)) { + Error("%s", err.c_str()); + return 1; + } + } + + RealDiskInterface disk_interface; + for (vector::iterator it = nodes.begin(), end = nodes.end(); + it != end; ++it) { + DepsLog::Deps* deps = deps_log_.GetDeps(*it); + if (!deps) { + printf("%s: deps not found\n", (*it)->path().c_str()); + continue; + } + + string err; + TimeStamp mtime = disk_interface.Stat((*it)->path(), &err); + if (mtime == -1) + Error("%s", err.c_str()); // Log and ignore Stat() errors; + int deps_count = deps->node_count - deps->outputs_count; + printf("%s: #dynouts %d, deps mtime %" PRId64 " (%s)\n", + (*it)->path().c_str(), deps->outputs_count, deps->mtime, + (!mtime || mtime > deps->mtime ? "STALE":"VALID")); + for (int i = deps_count; i < deps->node_count; ++i) + printf(" %s\n", deps->nodes[i]->path().c_str()); + printf("\n"); + } + + return 0; +} + +int NinjaMain::ToolOutputs(const Options*, int, char*[]) { + // Load dyndep files that exist, in order to load dynamic outputs + DyndepLoader dyndep_loader(&state_, &disk_interface_); + for (vector::iterator e = state_.edges_.begin(); + e != state_.edges_.end(); ++e) { + if (Node* dyndep = (*e)->dyndep_) { + std::string err; + dyndep_loader.LoadDyndeps(dyndep, &err); + } + } + + std::string err; + // Load dynamic outputs which may exist in the deps log + DepfileParserOptions depfileOptions; + ImplicitDepLoader implicit_dep_loader(&state_, &deps_log_, &disk_interface_, + &depfileOptions, nullptr); + for (vector::iterator e = state_.edges_.begin(); + e != state_.edges_.end(); ++e) { + string dynout = (*e)->GetUnescapedDynout(); + + if (!dynout.empty()) { + implicit_dep_loader.LoadImplicitOutputs(*e, &err); + } + } + + for (vector::iterator e = state_.edges_.begin(); + e != state_.edges_.end(); ++e) { + for (vector::iterator out_node = (*e)->outputs_.begin(); + out_node != (*e)->outputs_.end(); ++out_node) { + printf("%s: %s\n", (*out_node)->path().c_str(), + (*e)->rule_->name().c_str()); + } + } + + return 0; +} + int NinjaMain::ToolTargets(const Options* options, int argc, char* argv[]) { int depth = 1; if (argc >= 1) { @@ -1118,6 +1199,8 @@ const Tool* ChooseTool(const string& tool_name) { Tool::RUN_AFTER_LOAD, &NinjaMain::ToolInputs}, { "deps", "show dependencies stored in the deps log", Tool::RUN_AFTER_LOGS, &NinjaMain::ToolDeps }, + { "dynouts", "shows dynamic outputs stored in the deps log", + Tool::RUN_AFTER_LOGS, &NinjaMain::ToolDynOuts }, { "missingdeps", "check deps log dependencies on generated files", Tool::RUN_AFTER_LOGS, &NinjaMain::ToolMissingDeps }, { "graph", "output graphviz dot file for targets", @@ -1126,6 +1209,8 @@ const Tool* ChooseTool(const string& tool_name) { Tool::RUN_AFTER_LOGS, &NinjaMain::ToolQuery }, { "targets", "list targets by their rule or depth in the DAG", Tool::RUN_AFTER_LOAD, &NinjaMain::ToolTargets }, + { "outputs", "list all outputs of the build graph, including dynamic outputs stored in the deps log", + Tool::RUN_AFTER_LOGS, &NinjaMain::ToolOutputs }, { "compdb", "dump JSON compilation database to stdout", Tool::RUN_AFTER_LOAD, &NinjaMain::ToolCompilationDatabase }, { "recompact", "recompacts ninja-internal data structures",