Skip to content

Commit bee2e39

Browse files
committed
Add --depfile flag to inputs and multi-inputs tools.
This forces the load of depfile entries from either the Ninja log or explicit depfiles to be performed when visiting the graph, thus making them visible in the result. - graph.h, graph.cc: Add optional ImplicitDepLoader* argument to InputsCollector constructor, and if provided, use it to load deps during the visit if needed. - ninja.cc: Modify `inputs` and `multi-inputs` implementation to support a new `--depfile` flag, and create an ImplicitDepLoader instance when it is used to initialize the InputsCollector instances. - output_test.py: Add regression tests + fix minor typing issues. Note in particular that rules are defined with and without `deps = gcc` as this triggers different code paths in Ninja in the deps loader. Fixes #2618
1 parent 47ed2d2 commit bee2e39

File tree

5 files changed

+214
-33
lines changed

5 files changed

+214
-33
lines changed

doc/manual.asciidoc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,12 @@ executed in order, may be used to rebuild those targets, assuming that all
299299
output files are out of date.
300300
301301
`inputs`:: given a list of targets, print a list of all inputs used to
302-
rebuild those targets.
302+
rebuild those targets. By default, this only includes static inputs declared
303+
in the Ninja build plan itself.
304+
305+
Since Ninja 1.14, the `--depfile` flag can be used to include Ninja log / depfile
306+
inputs in the result. Note that if the build plan was regenerated since the last
307+
build, the log will contain stale entries, which may result in an incorrect output.
303308
_Available since Ninja 1.11._
304309
305310
`multi-inputs`:: print one or more sets of inputs required to build targets.
@@ -341,6 +346,10 @@ target3 file3.c
341346
+
342347
_Note that a given input may appear for several targets if it is used by more
343348
than one targets._
349+
350+
Like the `inputs` tool, this only reports static inputs from the build plan itself,
351+
but since Ninja 1.14, the `--depfile` flag can be used to also includes entries
352+
from the Ninja log or explicit depfiles.
344353
_Available since Ninja 1.13._
345354
346355
`clean`:: remove built files. By default, it removes all built files

misc/output_test.py

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def remove_non_visible_lines(raw_output: bytes) -> str:
5858
class BuildDir:
5959
def __init__(self, build_ninja: str):
6060
self.build_ninja = dedent(build_ninja)
61-
self.d = None
61+
self.d : None | tempfile.TemporaryDirectory[str] = None
6262

6363
def __enter__(self):
6464
self.d = tempfile.TemporaryDirectory()
@@ -70,8 +70,16 @@ def __enter__(self):
7070
def __exit__(self, exc_type, exc_val, exc_tb):
7171
self.d.cleanup()
7272

73+
def write_file(self, path: str, content: str) -> None:
74+
assert self.d
75+
file_path = os.path.join(self.d.name, path)
76+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
77+
with open(file_path, "wt") as f:
78+
f.write(content)
79+
7380
@property
7481
def path(self) -> str:
82+
assert self.d
7583
return os.path.realpath(self.d.name)
7684

7785

@@ -111,13 +119,13 @@ def run(
111119
try:
112120
if pipe:
113121
output = subprocess.check_output(
114-
[ninja_cmd], shell=True, cwd=self.d.name, env=env)
122+
[ninja_cmd], shell=True, cwd=self.path, env=env)
115123
elif platform.system() == 'Darwin':
116124
output = subprocess.check_output(['script', '-q', '/dev/null', 'bash', '-c', ninja_cmd],
117-
cwd=self.d.name, env=env)
125+
cwd=self.path, env=env)
118126
else:
119127
output = subprocess.check_output(['script', '-qfec', ninja_cmd, '/dev/null'],
120-
cwd=self.d.name, env=env)
128+
cwd=self.path, env=env)
121129
except subprocess.CalledProcessError as err:
122130
if print_err_output:
123131
sys.stdout.buffer.write(err.output)
@@ -366,8 +374,8 @@ def test_depfile_directory_creation(self) -> None:
366374
self.assertEqual(b.run('', pipe=True), dedent('''\
367375
[1/1] touch somewhere/out && echo "somewhere/out: extra" > somewhere_else/out.d
368376
'''))
369-
self.assertTrue(os.path.isfile(os.path.join(b.d.name, "somewhere", "out")))
370-
self.assertTrue(os.path.isfile(os.path.join(b.d.name, "somewhere_else", "out.d")))
377+
self.assertTrue(os.path.isfile(os.path.join(b.path, "somewhere", "out")))
378+
self.assertTrue(os.path.isfile(os.path.join(b.path, "somewhere_else", "out.d")))
371379

372380
def test_status(self) -> None:
373381
self.assertEqual(run(''), 'ninja: no work to do.\n')
@@ -468,6 +476,39 @@ def test_tool_inputs(self) -> None:
468476
f'''in1\0out1\0out 2\0'''
469477
)
470478

479+
def test_tool_inputs_with_depfile_entries(self) -> None:
480+
build_ninja = '''
481+
rule touch
482+
command = touch $out && echo "$out: $extras" > $depfile
483+
depfile = $out.d
484+
485+
build out1: touch in1
486+
deps = gcc
487+
extras = extra extra2
488+
489+
build out2: touch in2
490+
extras = extra3 extra4
491+
# No deps = gcc here intentionally
492+
'''
493+
with BuildDir(build_ninja) as b:
494+
# Do not run the build (empty deps log).
495+
self.assertEqual(b.run(flags='-t inputs out1'), 'in1\n')
496+
self.assertEqual(b.run(flags='-t inputs --depfile out1'), 'in1\n')
497+
498+
self.assertEqual(b.run(flags='-t inputs out2'), 'in2\n')
499+
self.assertEqual(b.run(flags='-t inputs --depfile out2'), 'in2\n')
500+
501+
# Run the plan and create the log entry for 'out'
502+
b.write_file("in1", "")
503+
b.write_file("in2", "")
504+
b.run('out1 out2')
505+
506+
# Check with a full deps log.
507+
self.assertEqual(b.run(flags='-t inputs out1'), 'in1\n')
508+
self.assertEqual(b.run(flags='-t inputs --depfile out1'), 'extra\nextra2\nin1\n')
509+
510+
self.assertEqual(b.run(flags='-t inputs out2'), 'in2\n')
511+
self.assertEqual(b.run(flags='-t inputs --depfile out2'), 'extra3\nextra4\nin2\n')
471512

472513
def test_tool_compdb_targets(self) -> None:
473514
plan = '''
@@ -556,6 +597,90 @@ def test_tool_multi_inputs(self) -> None:
556597
)
557598

558599

600+
def test_tool_multi_inputs_with_depfile_entries(self) -> None:
601+
build_ninja = '''
602+
rule touch1
603+
command = touch $out && echo "$out: extra extra2" > $depfile
604+
depfile = $out.d
605+
deps = gcc
606+
607+
rule touch2
608+
command = touch $out && echo "$out: extra extra3" > $depfile
609+
depfile = $out.d
610+
# Do not use deps = gcc here intentionally
611+
612+
build out1: touch1 in1
613+
build out2: touch2 in2
614+
'''
615+
616+
# Helper function to replace <TAB> with real tabs in input text.
617+
def t(text):
618+
if text[0] == '\n': # skip initial newline if any.
619+
text = text[1:]
620+
return text.replace("<TAB>", "\t")
621+
622+
with BuildDir(build_ninja) as b:
623+
# Run the plan and create the log entries for 'out1' and 'out2'
624+
b.write_file("in1", "1")
625+
b.write_file("in2", "2")
626+
b.run('out1 out2')
627+
628+
self.assertEqual(b.run(flags='-t multi-inputs'), t('''
629+
out1<TAB>in1
630+
out2<TAB>in2
631+
'''))
632+
self.assertEqual(b.run(flags='-t multi-inputs out1'), t('out1<TAB>in1\n'))
633+
self.assertEqual(b.run(flags='-t multi-inputs out2'), t('out2<TAB>in2\n'))
634+
self.assertEqual(b.run(flags='-t multi-inputs out1 out2'), t('''
635+
out1<TAB>in1
636+
out2<TAB>in2
637+
'''))
638+
self.assertEqual(b.run(flags='-t multi-inputs out2 out1'), t('''
639+
out2<TAB>in2
640+
out1<TAB>in1
641+
'''))
642+
643+
self.assertEqual(b.run(flags='-t multi-inputs --depfile'), t('''
644+
out1<TAB>in1
645+
out1<TAB>extra
646+
out1<TAB>extra2
647+
out2<TAB>in2
648+
out2<TAB>extra
649+
out2<TAB>extra3
650+
'''))
651+
652+
self.assertEqual(b.run(flags='-t multi-inputs --depfile out1'), t('''
653+
out1<TAB>in1
654+
out1<TAB>extra
655+
out1<TAB>extra2
656+
'''))
657+
658+
self.assertEqual(b.run(flags='-t multi-inputs --depfile out2'), t('''
659+
out2<TAB>in2
660+
out2<TAB>extra
661+
out2<TAB>extra3
662+
'''))
663+
664+
self.assertEqual(b.run(flags='-t multi-inputs --depfile out1 out2'), t('''
665+
out1<TAB>in1
666+
out1<TAB>extra
667+
out1<TAB>extra2
668+
out2<TAB>in2
669+
out2<TAB>extra
670+
out2<TAB>extra3
671+
'''))
672+
673+
self.assertEqual(b.run(flags='-t multi-inputs --depfile out2 out1'), t('''
674+
out2<TAB>in2
675+
out2<TAB>extra
676+
out2<TAB>extra3
677+
out1<TAB>in1
678+
out1<TAB>extra
679+
out1<TAB>extra2
680+
'''))
681+
682+
683+
559684
def test_explain_output(self):
560685
b = BuildDir('''\
561686
build .FORCE: phony

src/graph.cc

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,11 +762,26 @@ vector<Node*>::iterator ImplicitDepLoader::PreallocateSpace(Edge* edge,
762762
}
763763

764764
void InputsCollector::VisitNode(const Node* node) {
765-
const Edge* edge = node->in_edge();
765+
Edge* edge = node->in_edge();
766766

767767
if (!edge) // A source file.
768768
return;
769769

770+
if (implicit_dep_loader_ && !edge->deps_loaded_) {
771+
// Record that the deps were loaded in |deps_loaded_| as
772+
// multiple visits to the same edge can be performed by
773+
// repeated InputsCollector uses, as for the multi-inputs tool.
774+
edge->deps_loaded_ = true;
775+
776+
// Ignore errors when loading depfile entries.
777+
std::string err;
778+
if (!implicit_dep_loader_->LoadDeps(edge, &err) && !err.empty()) {
779+
// Print the error as a warning on stderr when an error occurred during
780+
// the load.
781+
Warning("%s", err.c_str());
782+
}
783+
}
784+
770785
// Add inputs of the producing edge to the result,
771786
// except if they are themselves produced by a phony
772787
// edge.

src/graph.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,12 @@ class EdgePriorityQueue:
440440
/// vector.
441441
///
442442
struct InputsCollector {
443+
/// Constructor allows passing a pointer to an optional ImplicitDepLoader
444+
/// which will be used to load implicit dependencies from the Ninja log
445+
/// or existing depfiles. Note that this will mutate the graph.
446+
explicit InputsCollector(ImplicitDepLoader* implicit_dep_loader = nullptr)
447+
: implicit_dep_loader_(implicit_dep_loader) {}
448+
443449
/// Visit a single @arg node during this collection.
444450
void VisitNode(const Node* node);
445451

@@ -459,6 +465,7 @@ struct InputsCollector {
459465
}
460466

461467
private:
468+
ImplicitDepLoader* implicit_dep_loader_ = nullptr;
462469
std::vector<const Node*> inputs_;
463470
std::set<const Node*> visited_nodes_;
464471
};

0 commit comments

Comments
 (0)