diff --git a/misc/output_test.py b/misc/output_test.py index c1f9f2579d..f48968bc9a 100755 --- a/misc/output_test.py +++ b/misc/output_test.py @@ -228,6 +228,71 @@ def test_tool_inputs(self) -> None: out2 ''') + self.assertEqual(run(plan, flags='-t inputs --dependency-order out3'), +'''in2 +in1 +out1 +out2 +implicit +order_only +''') + + # Verify that results are shell-escaped by default, unless --no-shell-escape + # is used. Also verify that phony outputs are never part of the results. + quote = '"' if platform.system() == "Windows" else "'" + + plan = ''' +rule cat + command = cat $in $out +build out1 : cat in1 +build out$ 2 : cat out1 +build out$ 3 : phony out$ 2 +build all: phony out$ 3 +''' + + # Quoting changes the order of results when sorting alphabetically. + self.assertEqual(run(plan, flags='-t inputs all'), +f'''{quote}out 2{quote} +in1 +out1 +''') + + self.assertEqual(run(plan, flags='-t inputs --no-shell-escape all'), +'''in1 +out 2 +out1 +''') + + # But not when doing dependency order. + self.assertEqual( + run( + plan, + flags='-t inputs --dependency-order all' + ), + f'''in1 +out1 +{quote}out 2{quote} +''') + + self.assertEqual( + run( + plan, + flags='-t inputs --dependency-order --no-shell-escape all' + ), + f'''in1 +out1 +out 2 +''') + + self.assertEqual( + run( + plan, + flags='-t inputs --dependency-order --no-shell-escape --print0 all' + ), + f'''in1\0out1\0out 2\0''' + ) + + def test_explain_output(self): b = BuildDir('''\ build .FORCE: phony diff --git a/src/graph.cc b/src/graph.cc index 143eabdfb4..f04ffb47c8 100644 --- a/src/graph.cc +++ b/src/graph.cc @@ -496,28 +496,6 @@ std::string EdgeEnv::MakePathList(const Node* const* const span, return result; } -void Edge::CollectInputs(bool shell_escape, - std::vector* out) const { - for (std::vector::const_iterator it = inputs_.begin(); - it != inputs_.end(); ++it) { - std::string path = (*it)->PathDecanonicalized(); - if (shell_escape) { - std::string unescaped; - unescaped.swap(path); -#ifdef _WIN32 - GetWin32EscapedString(unescaped, &path); -#else - GetShellEscapedString(unescaped, &path); -#endif - } -#if __cplusplus >= 201103L - out->push_back(std::move(path)); -#else - out->push_back(path); -#endif - } -} - std::string Edge::EvaluateCommand(const bool incl_rsp_file) const { string command = GetBinding("command"); if (incl_rsp_file) { @@ -779,3 +757,47 @@ vector::iterator ImplicitDepLoader::PreallocateSpace(Edge* edge, edge->implicit_deps_ += count; return edge->inputs_.end() - edge->order_only_deps_ - count; } + +void InputsCollector::VisitNode(const Node* node) { + const Edge* edge = node->in_edge(); + + if (!edge) // A source file. + return; + + // Add inputs of the producing edge to the result, + // except if they are themselves produced by a phony + // edge. + for (const Node* input : edge->inputs_) { + if (!visited_nodes_.insert(input).second) + continue; + + VisitNode(input); + + const Edge* input_edge = input->in_edge(); + if (!(input_edge && input_edge->is_phony())) { + inputs_.push_back(input); + } + } +} + +std::vector InputsCollector::GetInputsAsStrings( + bool shell_escape) const { + std::vector result; + result.reserve(inputs_.size()); + + for (const Node* input : inputs_) { + std::string unescaped = input->PathDecanonicalized(); + if (shell_escape) { + std::string path; +#ifdef _WIN32 + GetWin32EscapedString(unescaped, &path); +#else + GetShellEscapedString(unescaped, &path); +#endif + result.push_back(std::move(path)); + } else { + result.push_back(std::move(unescaped)); + } + } + return result; +} diff --git a/src/graph.h b/src/graph.h index 314c44296a..806260e5d7 100644 --- a/src/graph.h +++ b/src/graph.h @@ -201,9 +201,6 @@ struct Edge { void Dump(const char* prefix="") const; - // Append all edge explicit inputs to |*out|. Possibly with shell escaping. - void CollectInputs(bool shell_escape, std::vector* out) const; - // critical_path_weight is the priority during build scheduling. The // "critical path" between this edge's inputs and any target node is // the path which maximises the sum oof weights along that path. @@ -425,4 +422,41 @@ class EdgePriorityQueue: } }; +/// A class used to collect the transitive set of inputs from a given set +/// of starting nodes. Used to implement the `inputs` tool. +/// +/// When collecting inputs, the outputs of phony edges are always ignored +/// from the result, but are followed by the dependency walk. +/// +/// Usage is: +/// - Create instance. +/// - Call VisitNode() for each root node to collect inputs from. +/// - Call inputs() to retrieve the list of input node pointers. +/// - Call GetInputsAsStrings() to retrieve the list of inputs as a string +/// vector. +/// +struct InputsCollector { + /// Visit a single @arg node during this collection. + void VisitNode(const Node* node); + + /// Retrieve list of visited input nodes. A dependency always appears + /// before its dependents in the result, but final order depends on the + /// order of the VisitNode() calls performed before this. + const std::vector& inputs() const { return inputs_; } + + /// Same as inputs(), but returns the list of visited nodes as a list of + /// strings, with optional shell escaping. + std::vector GetInputsAsStrings(bool shell_escape = false) const; + + /// Reset collector state. + void Reset() { + inputs_.clear(); + visited_nodes_.clear(); + } + + private: + std::vector inputs_; + std::set visited_nodes_; +}; + #endif // NINJA_GRAPH_H_ diff --git a/src/graph_test.cc b/src/graph_test.cc index f909b906fd..6c654eeb32 100644 --- a/src/graph_test.cc +++ b/src/graph_test.cc @@ -215,28 +215,90 @@ TEST_F(GraphTest, RootNodes) { } } -TEST_F(GraphTest, CollectInputs) { +TEST_F(GraphTest, InputsCollector) { + // Build plan for the following graph: + // + // in1 + // |___________ + // | | + // === === + // | | + // out1 mid1 + // | ____|_____ + // | | | + // | === ======= + // | | | | + // | out2 out3 out4 + // | | | + // =======phony====== + // | + // all + // + ASSERT_NO_FATAL_FAILURE(AssertParse(&state_, + "build out1: cat in1\n" + "build mid1: cat in1\n" + "build out2: cat mid1\n" + "build out3 out4: cat mid1\n" + "build all: phony out1 out2 out3\n")); + + InputsCollector collector; + + // Start visit from out1, this should add in1 to the inputs. + collector.Reset(); + collector.VisitNode(GetNode("out1")); + auto inputs = collector.GetInputsAsStrings(); + ASSERT_EQ(1u, inputs.size()); + EXPECT_EQ("in1", inputs[0]); + + // Add a visit from out2, this should add mid1. + collector.VisitNode(GetNode("out2")); + inputs = collector.GetInputsAsStrings(); + ASSERT_EQ(2u, inputs.size()); + EXPECT_EQ("in1", inputs[0]); + EXPECT_EQ("mid1", inputs[1]); + + // Another visit from all, this should add out1, out2 and out3, + // but not out4. + collector.VisitNode(GetNode("all")); + inputs = collector.GetInputsAsStrings(); + ASSERT_EQ(5u, inputs.size()); + EXPECT_EQ("in1", inputs[0]); + EXPECT_EQ("mid1", inputs[1]); + EXPECT_EQ("out1", inputs[2]); + EXPECT_EQ("out2", inputs[3]); + EXPECT_EQ("out3", inputs[4]); + + collector.Reset(); + + // Starting directly from all, will add out1 before mid1 compared + // to the previous example above. + collector.VisitNode(GetNode("all")); + inputs = collector.GetInputsAsStrings(); + ASSERT_EQ(5u, inputs.size()); + EXPECT_EQ("in1", inputs[0]); + EXPECT_EQ("out1", inputs[1]); + EXPECT_EQ("mid1", inputs[2]); + EXPECT_EQ("out2", inputs[3]); + EXPECT_EQ("out3", inputs[4]); +} + +TEST_F(GraphTest, InputsCollectorWithEscapes) { ASSERT_NO_FATAL_FAILURE(AssertParse( &state_, "build out$ 1: cat in1 in2 in$ with$ space | implicit || order_only\n")); - std::vector inputs; - Edge* edge = GetNode("out 1")->in_edge(); - - // Test without shell escaping. - inputs.clear(); - edge->CollectInputs(false, &inputs); - EXPECT_EQ(5u, inputs.size()); + InputsCollector collector; + collector.VisitNode(GetNode("out 1")); + auto inputs = collector.GetInputsAsStrings(); + ASSERT_EQ(5u, inputs.size()); EXPECT_EQ("in1", inputs[0]); EXPECT_EQ("in2", inputs[1]); EXPECT_EQ("in with space", inputs[2]); EXPECT_EQ("implicit", inputs[3]); EXPECT_EQ("order_only", inputs[4]); - // Test with shell escaping. - inputs.clear(); - edge->CollectInputs(true, &inputs); - EXPECT_EQ(5u, inputs.size()); + inputs = collector.GetInputsAsStrings(true); + ASSERT_EQ(5u, inputs.size()); EXPECT_EQ("in1", inputs[0]); EXPECT_EQ("in2", inputs[1]); #ifdef _WIN32 diff --git a/src/ninja.cc b/src/ninja.cc index 2902359f15..9b77679786 100644 --- a/src/ninja.cc +++ b/src/ninja.cc @@ -761,43 +761,50 @@ int NinjaMain::ToolCommands(const Options* options, int argc, char* argv[]) { return 0; } -void CollectInputs(Edge* edge, std::set* seen, - std::vector* result) { - if (!edge) - return; - if (!seen->insert(edge).second) - return; - - for (vector::iterator in = edge->inputs_.begin(); - in != edge->inputs_.end(); ++in) - CollectInputs((*in)->in_edge(), seen, result); - - if (!edge->is_phony()) { - edge->CollectInputs(true, result); - } -} - int NinjaMain::ToolInputs(const Options* options, int argc, char* argv[]) { // The inputs tool uses getopt, and expects argv[0] to contain the name of // the tool, i.e. "inputs". argc++; argv--; + + bool print0 = false; + bool shell_escape = true; + bool dependency_order = false; + optind = 1; int opt; const option kLongOptions[] = { { "help", no_argument, NULL, 'h' }, + { "no-shell-escape", no_argument, NULL, 'E' }, + { "print0", no_argument, NULL, '0' }, + { "dependency-order", no_argument, NULL, + 'd' }, { NULL, 0, NULL, 0 } }; - while ((opt = getopt_long(argc, argv, "h", kLongOptions, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "h0Ed", kLongOptions, NULL)) != -1) { switch (opt) { + case 'd': + dependency_order = true; + break; + case 'E': + shell_escape = false; + break; + case '0': + print0 = true; + break; case 'h': default: // clang-format off printf( "Usage '-t inputs [options] [targets]\n" "\n" -"List all inputs used for a set of targets. Note that this includes\n" -"explicit, implicit and order-only inputs, but not validation ones.\n\n" +"List all inputs used for a set of targets, sorted in dependency order.\n" +"Note that by default, results are shell escaped, and sorted alphabetically,\n" +"and never include validation target paths.\n\n" "Options:\n" -" -h, --help Print this message.\n"); +" -h, --help Print this message.\n" +" -0, --print0 Use \\0, instead of \\n as a line terminator.\n" +" -E, --no-shell-escape Do not shell escape the result.\n" +" -d, --dependency-order Sort results by dependency order.\n" + ); // clang-format on return 1; } @@ -805,25 +812,31 @@ int NinjaMain::ToolInputs(const Options* options, int argc, char* argv[]) { argv += optind; argc -= optind; - vector nodes; - string err; + std::vector nodes; + std::string err; if (!CollectTargetsFromArgs(argc, argv, &nodes, &err)) { Error("%s", err.c_str()); return 1; } - std::set seen; - std::vector result; - for (vector::iterator in = nodes.begin(); in != nodes.end(); ++in) - CollectInputs((*in)->in_edge(), &seen, &result); + InputsCollector collector; + for (const Node* node : nodes) + collector.VisitNode(node); - // Make output deterministic by sorting then removing duplicates. - std::sort(result.begin(), result.end()); - result.erase(std::unique(result.begin(), result.end()), result.end()); - - for (size_t n = 0; n < result.size(); ++n) - puts(result[n].c_str()); + std::vector inputs = collector.GetInputsAsStrings(shell_escape); + if (!dependency_order) + std::sort(inputs.begin(), inputs.end()); + if (print0) { + for (const std::string& input : inputs) { + fwrite(input.c_str(), input.size(), 1, stdout); + fputc('\0', stdout); + } + fflush(stdout); + } else { + for (const std::string& input : inputs) + puts(input.c_str()); + } return 0; }