Merge pull request #2485 from digit-google/fix-inputs-tool

Fix `inputs` tool logic and add new formatting options.
This commit is contained in:
Jan Niklas Hasse 2024-09-19 08:11:23 +02:00 committed by GitHub
commit 41ecb09d36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 265 additions and 69 deletions

View File

@ -228,6 +228,71 @@ out1
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

View File

@ -496,28 +496,6 @@ std::string EdgeEnv::MakePathList(const Node* const* const span,
return result;
}
void Edge::CollectInputs(bool shell_escape,
std::vector<std::string>* out) const {
for (std::vector<Node*>::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<Node*>::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<std::string> InputsCollector::GetInputsAsStrings(
bool shell_escape) const {
std::vector<std::string> 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;
}

View File

@ -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<std::string>* 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 @@ public:
}
};
/// 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<const Node*>& 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<std::string> GetInputsAsStrings(bool shell_escape = false) const;
/// Reset collector state.
void Reset() {
inputs_.clear();
visited_nodes_.clear();
}
private:
std::vector<const Node*> inputs_;
std::set<const Node*> visited_nodes_;
};
#endif // NINJA_GRAPH_H_

View File

@ -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<std::string> 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

View File

@ -761,43 +761,50 @@ int NinjaMain::ToolCommands(const Options* options, int argc, char* argv[]) {
return 0;
}
void CollectInputs(Edge* edge, std::set<Edge*>* seen,
std::vector<std::string>* result) {
if (!edge)
return;
if (!seen->insert(edge).second)
return;
for (vector<Node*>::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<Node*> nodes;
string err;
std::vector<Node*> nodes;
std::string err;
if (!CollectTargetsFromArgs(argc, argv, &nodes, &err)) {
Error("%s", err.c_str());
return 1;
}
std::set<Edge*> seen;
std::vector<std::string> result;
for (vector<Node*>::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<std::string> 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;
}