[gn] Split SOURCE_SET phony targets to exclude additional outputs from linking Split the phony target generated for a `source_set` into two targets: 1. The default phony target (e.g., `phony/foo/bar`) which depends on both object files and additional outputs (like `.dwo` files). 2. A link-only phony target (e.g., `phony/foo/bar.linkdeps`) which depends only on object files. When other targets depend on this `source_set`, they will now use the `.linkdeps` suffixed target for their order-only dependencies. This is important for build systems like Siso that support remote linking. If the link step indirectly depends on `.dwo` files through the phony target, Siso might include those `.dwo` files as inputs for the remote link command. Since `.dwo` files are only needed for debugging and not for linking, uploading them would be a waste of bandwidth and time. By splitting the phony target, we prevent `.dwo` files from being included as inputs for the link step. Bug: 502431091 Change-Id: I574063a57f537a2c5f9662bb39202ecc4cf92761 Reviewed-on: https://gn-review.googlesource.com/c/gn/+/22160 Commit-Queue: David Turner <digit@google.com> Reviewed-by: Junji Watanabe <jwata@google.com> Reviewed-by: David Turner <digit@google.com>
diff --git a/src/gn/ninja_binary_target_writer_unittest.cc b/src/gn/ninja_binary_target_writer_unittest.cc index 51b3369..e8a6f51 100644 --- a/src/gn/ninja_binary_target_writer_unittest.cc +++ b/src/gn/ninja_binary_target_writer_unittest.cc
@@ -49,8 +49,9 @@ " source_file_part = input2.cc\n" " source_name_part = input2\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.input1.o " - "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o " + "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps\n"; std::string out_str = out.str(); EXPECT_EQ(expected, out_str); } @@ -147,7 +148,8 @@ " source_file_part = source1.cc\n" " source_name_part = source1\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.source1.o\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.source1.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps\n"; std::string out_str = out.str(); EXPECT_EQ(expected, out_str); } @@ -189,8 +191,9 @@ " source_file_part = source2.cc\n" " source_name_part = source2\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.source1.o " - "obj/foo/bar.source2.o\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.source1.o " + "obj/foo/bar.source2.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps\n"; std::string out_str = out.str(); EXPECT_EQ(expected, out_str); }
diff --git a/src/gn/ninja_c_binary_target_writer.cc b/src/gn/ninja_c_binary_target_writer.cc index 14f95ed..3121718 100644 --- a/src/gn/ninja_c_binary_target_writer.cc +++ b/src/gn/ninja_c_binary_target_writer.cc
@@ -12,6 +12,7 @@ #include <sstream> #include "base/strings/string_util.h" +#include "gn/builtin_tool.h" #include "gn/c_substitution_type.h" #include "gn/config_values_extractors.h" #include "gn/deps_iterator.h" @@ -531,6 +532,12 @@ void NinjaCBinaryTargetWriter::WriteSourceSetStamp( const std::vector<OutputFile>& object_files) { + // If the target doesn't have a dependency output, we shouldn't write + // anything. + if (!target_->has_dependency_output()) { + return; + } + // The stamp rule for source sets is generally not used, since targets that // depend on this will reference the object files directly. However, writing // this rule allows the user to type the name of the target and get a build @@ -545,11 +552,53 @@ std::vector<OutputFile> order_only_deps; for (auto* dep : classified_deps.non_linkable_deps) { if (dep->has_dependency_output()) { - order_only_deps.push_back(dep->dependency_output()); + OutputFile dep_output = dep->dependency_output(); + if (dep->output_type() == Target::SOURCE_SET) { + dep_output.value().append(".linkdeps"); + } + order_only_deps.push_back(dep_output); } } - WriteStampOrPhonyForTarget(object_files, order_only_deps); + // 1. Link-only phony target (.linkdeps) containing only object files. + std::vector<OutputFile> link_files; + const BuildSettings* build_settings = settings_->build_settings(); + for (const auto& file : object_files) { + if (file.AsSourceFile(build_settings).IsObjectType()) { + link_files.push_back(file); + } + } + + OutputFile link_phony = target_->dependency_output(); + link_phony.value().append(".linkdeps"); + + out_ << "build "; + path_output_.WriteFile(out_, link_phony); + out_ << ": " << BuiltinTool::kBuiltinToolPhony; + path_output_.WriteFiles(out_, link_files); + if (!order_only_deps.empty()) { + out_ << " ||"; + path_output_.WriteFiles(out_, order_only_deps); + } + out_ << std::endl; + + // 2. Default phony target containing all files (including additional + // outputs). Depend on the .link target to avoid duplicating object files. + out_ << "build "; + path_output_.WriteFile(out_, target_->dependency_output()); + out_ << ": " << BuiltinTool::kBuiltinToolPhony; + out_ << " "; + path_output_.WriteFile(out_, link_phony); + + // Collect non-object files (additional outputs) to add here. + std::vector<OutputFile> non_object_files; + for (const auto& file : object_files) { + if (!file.AsSourceFile(build_settings).IsObjectType()) { + non_object_files.push_back(file); + } + } + path_output_.WriteFiles(out_, non_object_files); + out_ << std::endl; } void NinjaCBinaryTargetWriter::WriteLinkerStuff( @@ -763,7 +812,11 @@ for (auto* non_linkable_dep : non_linkable_deps) { if (non_linkable_dep->has_dependency_output()) { out_ << " "; - path_output_.WriteFile(out_, non_linkable_dep->dependency_output()); + OutputFile dep_output = non_linkable_dep->dependency_output(); + if (non_linkable_dep->output_type() == Target::SOURCE_SET) { + dep_output.value().append(".linkdeps"); + } + path_output_.WriteFile(out_, dep_output); } } }
diff --git a/src/gn/ninja_c_binary_target_writer_unittest.cc b/src/gn/ninja_c_binary_target_writer_unittest.cc index beb7409..4b0219b 100644 --- a/src/gn/ninja_c_binary_target_writer_unittest.cc +++ b/src/gn/ninja_c_binary_target_writer_unittest.cc
@@ -66,8 +66,9 @@ " source_file_part = input2.cc\n" " source_name_part = input2\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.input1.o " - "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o " + "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps\n"; std::string out_str = out.str(); EXPECT_EQ(expected, out_str); } @@ -98,7 +99,7 @@ // order. "build ./libshlib.so: solink obj/foo/bar.input1.o " "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj " - "|| phony/foo/bar\n" + "|| phony/foo/bar.linkdeps\n" " ldflags =\n" " libs =\n" " frameworks =\n" @@ -132,7 +133,7 @@ "\n" // There are no sources so there are no params to alink. (In practice // this will probably fail in the archive tool.) - "build obj/foo/libstlib.a: alink || phony/foo/bar\n" + "build obj/foo/libstlib.a: alink || phony/foo/bar.linkdeps\n" " arflags =\n" " output_extension =\n" " output_dir =\n"; @@ -161,7 +162,7 @@ // order. "build obj/foo/libstlib.a: alink obj/foo/bar.input1.o " "obj/foo/bar.input2.o ../../foo/input3.o ../../foo/input4.obj " - "|| phony/foo/bar\n" + "|| phony/foo/bar.linkdeps\n" " arflags =\n" " output_extension =\n" " output_dir =\n"; @@ -218,11 +219,166 @@ " source_file_part = input1.cc\n" " source_name_part = input1\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.input1.o obj/foo/input1.dwo\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps obj/foo/input1.dwo\n"; EXPECT_EQ(expected, out.str()); } +TEST_F(NinjaCBinaryTargetWriterTest, AdditionalOutputsSourceSet) { + Err err; + TestWithScope setup; + + Value outputs_value(nullptr, Value::LIST); + outputs_value.list_value().push_back( + Value(nullptr, "{{target_out_dir}}/{{source_name_part}}.dwo")); + + Config config(setup.settings(), Label(SourceDir("//foo/"), "split_dwarf")); + config.visibility().SetPublic(); + + std::vector<SubstitutionPattern> patterns; + for (const auto& v : outputs_value.list_value()) { + SubstitutionPattern pattern; + ASSERT_TRUE(pattern.Parse(v, &err)); + patterns.push_back(std::move(pattern)); + } + config.own_values().c_additional_outputs() = std::move(patterns); + ASSERT_TRUE(config.OnResolved(&err)); + + Target target(setup.settings(), Label(SourceDir("//foo/"), "bar")); + target.set_output_type(Target::SOURCE_SET); + target.visibility().SetPublic(); + target.sources().push_back(SourceFile("//foo/input1.cc")); + target.source_types_used().Set(SourceFile::SOURCE_CPP); + target.configs().push_back(LabelConfigPair(&config)); + target.SetToolchain(setup.toolchain()); + ASSERT_TRUE(target.OnResolved(&err)); + + std::ostringstream out; + NinjaCBinaryTargetWriter writer(&target, out); + writer.Run(); + + const char expected[] = + "defines =\n" + "include_dirs =\n" + "cflags =\n" + "cflags_cc =\n" + "root_out_dir = .\n" + "target_gen_dir = gen/foo\n" + "target_out_dir = obj/foo\n" + "target_output_name = bar\n" + "\n" + "build obj/foo/bar.input1.o obj/foo/input1.dwo: cxx " + "../../foo/input1.cc\n" + " source_file_part = input1.cc\n" + " source_name_part = input1\n" + "\n" + "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps obj/foo/input1.dwo\n"; + + EXPECT_EQ(expected, out.str()); +} + +TEST_F(NinjaCBinaryTargetWriterTest, + SourceSetWithAdditionalOutputsToStaticLib) { + Err err; + TestWithScope setup; + + Value outputs_value(nullptr, Value::LIST); + outputs_value.list_value().push_back( + Value(nullptr, "{{target_out_dir}}/{{source_name_part}}.dwo")); + + Config config(setup.settings(), Label(SourceDir("//foo/"), "split_dwarf")); + config.visibility().SetPublic(); + + std::vector<SubstitutionPattern> patterns; + for (const auto& v : outputs_value.list_value()) { + SubstitutionPattern pattern; + ASSERT_TRUE(pattern.Parse(v, &err)); + patterns.push_back(std::move(pattern)); + } + config.own_values().c_additional_outputs() = std::move(patterns); + ASSERT_TRUE(config.OnResolved(&err)); + + // Source set B (dependee) + Target target_b(setup.settings(), Label(SourceDir("//foo/"), "b")); + target_b.set_output_type(Target::SOURCE_SET); + target_b.sources().push_back(SourceFile("//foo/b.cc")); + target_b.sources().push_back(SourceFile("//foo/b2.cc")); + target_b.source_types_used().Set(SourceFile::SOURCE_CPP); + target_b.visibility().SetPublic(); + target_b.configs().push_back(LabelConfigPair(&config)); + target_b.SetToolchain(setup.toolchain()); + ASSERT_TRUE(target_b.OnResolved(&err)) << err.message(); + + // Static library A (depender) + Target target_a(setup.settings(), Label(SourceDir("//foo/"), "a")); + target_a.set_output_type(Target::STATIC_LIBRARY); + target_a.sources().push_back(SourceFile("//foo/a.cc")); + target_a.source_types_used().Set(SourceFile::SOURCE_CPP); + target_a.private_deps().push_back(LabelTargetPair(&target_b)); + target_a.SetToolchain(setup.toolchain()); + ASSERT_TRUE(target_a.OnResolved(&err)) << err.message(); + + // Verify B's output + { + std::ostringstream out; + NinjaCBinaryTargetWriter writer(&target_b, out); + writer.Run(); + + const char expected[] = + "defines =\n" + "include_dirs =\n" + "cflags =\n" + "cflags_cc =\n" + "root_out_dir = .\n" + "target_gen_dir = gen/foo\n" + "target_out_dir = obj/foo\n" + "target_output_name = b\n" + "\n" + "build obj/foo/b.b.o obj/foo/b.dwo: cxx ../../foo/b.cc\n" + " source_file_part = b.cc\n" + " source_name_part = b\n" + "build obj/foo/b.b2.o obj/foo/b2.dwo: cxx ../../foo/b2.cc\n" + " source_file_part = b2.cc\n" + " source_name_part = b2\n" + "\n" + "build phony/foo/b.linkdeps: phony obj/foo/b.b.o obj/foo/b.b2.o\n" + "build phony/foo/b: phony phony/foo/b.linkdeps obj/foo/b.dwo " + "obj/foo/b2.dwo\n"; + + EXPECT_EQ(expected, out.str()); + } + + // Verify A's output + { + std::ostringstream out; + NinjaCBinaryTargetWriter writer(&target_a, out); + writer.Run(); + + const char expected[] = + "defines =\n" + "include_dirs =\n" + "cflags =\n" + "cflags_cc =\n" + "root_out_dir = .\n" + "target_gen_dir = gen/foo\n" + "target_out_dir = obj/foo\n" + "target_output_name = liba\n" + "\n" + "build obj/foo/liba.a.o: cxx ../../foo/a.cc\n" + " source_file_part = a.cc\n" + " source_name_part = a\n" + "\n" + "build obj/foo/liba.a: alink obj/foo/liba.a.o || phony/foo/b.linkdeps\n" + " arflags =\n" + " output_extension =\n" + " output_dir =\n"; + + EXPECT_EQ(expected, out.str()); + } +} + TEST_F(NinjaCBinaryTargetWriterTest, EscapeDefines) { TestWithScope setup; Err err; @@ -478,10 +634,12 @@ " source_file_part = generated.cc\n" " source_name_part = generated\n" "\n" - "build phony/foo/gen_obj: phony obj/BUILD_DIR/gen_obj.generated.o" + "build phony/foo/gen_obj.linkdeps: phony " + "obj/BUILD_DIR/gen_obj.generated.o" // The order-only dependency here is strictly unnecessary since the // sources list this as an order-only dep. - " || phony/foo/generate\n"; + " || phony/foo/generate\n" + "build phony/foo/gen_obj: phony phony/foo/gen_obj.linkdeps\n"; std::string obj_str = obj_out.str(); EXPECT_EQ(std::string(obj_expected), obj_str); @@ -515,7 +673,7 @@ // The order-only dependency here is strictly unnecessary since // obj/out/Debug/gen_obj.generated.o has dependency to // obj/foo/gen_obj.stamp - " || phony/foo/gen_obj\n" + " || phony/foo/gen_obj.linkdeps\n" " ldflags =\n" " libs =\n" " frameworks =\n" @@ -777,8 +935,9 @@ " source_file_part = inter.cc\n" " source_name_part = inter\n" "\n" - "build phony/foo/inter: phony obj/foo/inter.inter.o || " - "./data_target\n"; + "build phony/foo/inter.linkdeps: phony obj/foo/inter.inter.o || " + "./data_target\n" + "build phony/foo/inter: phony phony/foo/inter.linkdeps\n"; EXPECT_EQ(inter_expected, inter_out.str()); // Final target. @@ -814,7 +973,7 @@ " source_name_part = final\n" "\n" "build ./exe: link obj/foo/exe.final.o obj/foo/inter.inter.o || " - "phony/foo/inter\n" + "phony/foo/inter.linkdeps\n" " ldflags =\n" " libs =\n" " frameworks =\n" @@ -1016,9 +1175,11 @@ " source_file_part = input2.c\n" " source_name_part = input2\n" "\n" - "build withpch/phony/foo/no_pch_target: " + "build withpch/phony/foo/no_pch_target.linkdeps: " "phony withpch/obj/foo/no_pch_target.input1.o " - "withpch/obj/foo/no_pch_target.input2.o\n"; + "withpch/obj/foo/no_pch_target.input2.o\n" + "build withpch/phony/foo/no_pch_target: " + "phony withpch/phony/foo/no_pch_target.linkdeps\n"; EXPECT_EQ(no_pch_expected, out.str()); } @@ -1078,12 +1239,14 @@ " source_file_part = input2.c\n" " source_name_part = input2\n" "\n" - "build withpch/phony/foo/pch_target: phony " + "build withpch/phony/foo/pch_target.linkdeps: phony " "withpch/obj/foo/pch_target.input1.o " "withpch/obj/foo/pch_target.input2.o " // The precompiled object files were added to the outputs. "withpch/obj/build/pch_target.precompile.c.o " - "withpch/obj/build/pch_target.precompile.cc.o\n"; + "withpch/obj/build/pch_target.precompile.cc.o\n" + "build withpch/phony/foo/pch_target: phony " + "withpch/phony/foo/pch_target.linkdeps\n"; EXPECT_EQ(pch_win_expected, out.str()); } } @@ -1162,9 +1325,11 @@ " source_file_part = input2.c\n" " source_name_part = input2\n" "\n" - "build withpch/phony/foo/no_pch_target: " + "build withpch/phony/foo/no_pch_target.linkdeps: " "phony withpch/obj/foo/no_pch_target.input1.o " - "withpch/obj/foo/no_pch_target.input2.o\n"; + "withpch/obj/foo/no_pch_target.input2.o\n" + "build withpch/phony/foo/no_pch_target: " + "phony withpch/phony/foo/no_pch_target.linkdeps\n"; EXPECT_EQ(no_pch_expected, out.str()); } @@ -1222,9 +1387,11 @@ " source_file_part = input2.c\n" " source_name_part = input2\n" "\n" - "build withpch/phony/foo/pch_target: " + "build withpch/phony/foo/pch_target.linkdeps: " "phony withpch/obj/foo/pch_target.input1.o " - "withpch/obj/foo/pch_target.input2.o\n"; + "withpch/obj/foo/pch_target.input2.o\n" + "build withpch/phony/foo/pch_target: " + "phony withpch/phony/foo/pch_target.linkdeps\n"; EXPECT_EQ(pch_gcc_expected, out.str()); } } @@ -1293,8 +1460,9 @@ " source_file_part = input2.cc\n" " source_name_part = input2\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.input1.o " - "obj/foo/bar.input2.o\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o " + "obj/foo/bar.input2.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps\n"; EXPECT_EQ(expected, out.str()); } @@ -1370,8 +1538,9 @@ " source_file_part = input2.cc\n" " source_name_part = input2\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.input1.o " - "obj/foo/bar.input2.o\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o " + "obj/foo/bar.input2.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps\n"; EXPECT_EQ(expected, out.str()); } @@ -1425,8 +1594,9 @@ " source_file_part = input2.cc\n" " source_name_part = input2\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.input1.o " - "obj/foo/bar.input2.o\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.input1.o " + "obj/foo/bar.input2.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps\n"; EXPECT_EQ(expected, out.str()); } @@ -1659,8 +1829,8 @@ "obj/dylib/libdylib.so | " "obj/pub_in_staticlib/libpub_in_staticlib.rlib " "obj/priv_in_staticlib/libpriv_in_staticlib.rlib || " - "phony/pub_sset_in_staticlib/pub_sset_in_staticlib " - "phony/priv_sset_in_staticlib/priv_sset_in_staticlib\n" + "phony/pub_sset_in_staticlib/pub_sset_in_staticlib.linkdeps " + "phony/priv_sset_in_staticlib/priv_sset_in_staticlib.linkdeps\n" " ldflags =\n" " libs =\n" " frameworks =\n" @@ -2395,7 +2565,8 @@ " source_file_part = bar.modulemap\n" " source_name_part = bar\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.bar.o obj/foo/bar.bar.pcm\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.bar.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps obj/foo/bar.bar.pcm\n"; std::string out_str = out.str(); EXPECT_EQ(expected, out_str); } @@ -2435,9 +2606,9 @@ "obj/foo/file2.o: swift ../../foo/file1.swift ../../foo/file2.swift\n" " restat = 1\n" "\n" - "build phony/foo/foo: phony" - " gen/foo/foo.h obj/foo/Foo.swiftmodule" - " obj/foo/file1.o obj/foo/file2.o\n"; + "build phony/foo/foo.linkdeps: phony obj/foo/file1.o obj/foo/file2.o\n" + "build phony/foo/foo: phony phony/foo/foo.linkdeps gen/foo/foo.h " + "obj/foo/Foo.swiftmodule\n"; const std::string out_str = out.str(); EXPECT_EQ(expected, out_str); @@ -2473,9 +2644,10 @@ "../../bar/bar.swift || phony/foo/foo\n" " restat = 1\n" "\n" - "build phony/bar/bar: phony" - " gen/bar/bar.h obj/bar/Bar.swiftmodule obj/bar/bar.o " - "|| phony/foo/foo\n"; + "build phony/bar/bar.linkdeps: phony obj/bar/bar.o || " + "phony/foo/foo.linkdeps\n" + "build phony/bar/bar: phony phony/bar/bar.linkdeps gen/bar/bar.h " + "obj/bar/Bar.swiftmodule\n"; const std::string out_str = out.str(); EXPECT_EQ(expected, out_str); @@ -2519,9 +2691,10 @@ "../../bar/bar.swift || phony/bar/group phony/foo/foo\n" " restat = 1\n" "\n" - "build phony/bar/bar: phony" - " gen/bar/bar.h obj/bar/Bar.swiftmodule obj/bar/bar.o " - "|| phony/bar/group phony/foo/foo\n"; + "build phony/bar/bar.linkdeps: phony obj/bar/bar.o || phony/bar/group " + "phony/foo/foo.linkdeps\n" + "build phony/bar/bar: phony phony/bar/bar.linkdeps gen/bar/bar.h " + "obj/bar/Bar.swiftmodule\n"; const std::string out_str = out.str(); EXPECT_EQ(expected, out_str); @@ -2551,7 +2724,7 @@ "\n" "build ./bar: link obj/foo/file1.o obj/foo/file2.o " "| obj/foo/Foo.swiftmodule " - "|| phony/foo/foo\n" + "|| phony/foo/foo.linkdeps\n" " ldflags =\n" " libs =\n" " frameworks =\n" @@ -3058,7 +3231,8 @@ " source_file_part = source1.cc\n" " source_name_part = source1\n" "\n" - "build phony/foo/bar: phony obj/foo/bar.source1.o\n"; + "build phony/foo/bar.linkdeps: phony obj/foo/bar.source1.o\n" + "build phony/foo/bar: phony phony/foo/bar.linkdeps\n"; writer.Run(); std::string ninja_str = ninja_out.str(); EXPECT_EQ(expected_ninja, ninja_str); @@ -3204,7 +3378,8 @@ " source_file_part = root.cc\n" " source_name_part = root\n" "\n" - "build phony/foo/root: phony obj/foo/root.root.o\n"; + "build phony/foo/root.linkdeps: phony obj/foo/root.root.o\n" + "build phony/foo/root: phony phony/foo/root.linkdeps\n"; EXPECT_EQ(expected_root_ninja, root_ninja_str); }