Add public_inputs variable to action targets and propagate dependencies This change introduces the `public_inputs` variable to `action` and `action_foreach` targets. The specified files are treated as implicit inputs of dependent targets, meaning they propagate to direct dependents and transitively across targets depending via `public_deps`. To avoid bloat in the generated `.ninja` files, we compile public inputs into a dedicated `.public_inputs` (or `.public_inputs.stamp`) phony/stamp target for each action. Dependent targets then implicitly depend on this dedicated target, rather than listing all files explicitly in their build lines. I'll use this for input files propagation in typescript compilation in devtools-frontend. Bug: 531579666 Change-Id: If078534cdb51ab8a61e6f8095e0c29aa72bc283c Reviewed-on: https://gn-review.googlesource.com/c/gn/+/23700 Reviewed-by: Junji Watanabe <jwata@google.com> Reviewed-by: David Turner <digit@google.com> Commit-Queue: Takuto Ikuta <tikuta@google.com>
diff --git a/docs/reference.md b/docs/reference.md index a8d370a..66a0f8d 100644 --- a/docs/reference.md +++ b/docs/reference.md
@@ -160,6 +160,7 @@ * [public: [file list] Declare public header files for a target.](#var_public) * [public_configs: [label list] Configs applied to dependents.](#var_public_configs) * [public_deps: [label list] Declare public dependencies.](#var_public_deps) + * [public_inputs: [file list] Declare public inputs.](#var_public_inputs) * [rebase: [boolean] Rebase collected metadata as files.](#var_rebase) * [response_file_contents: [string list] Contents of .rsp file for actions.](#var_response_file_contents) * [rustflags: [string list] Flags passed to the Rust compiler.](#var_rustflags) @@ -6876,6 +6877,43 @@ public_deps = [ ":c" ] } ``` +### <a name="var_public_inputs"></a>**public_inputs**: Declare public inputs. [Back to Top](#gn-reference) + +``` + Inputs to a target that should be treated as implicit inputs of any dependent + targets. + + If target A declares public_inputs and target B depends on A (either via + deps or public_deps), B will inherit A's public_inputs. + + If target B depends on A via public_deps, B's public_inputs will also + propagate to any targets depending on B. + + This is particularly useful for actions that generate source code which + contain implicit imports/includes of the files declared in public_inputs. + Dependent targets will automatically inherit these dependencies and trigger + rebuilds when the public inputs change. + + See also "inputs" and "public_deps". + + Example + + action("A") { + ... + public_inputs = [ "a.in" ] + } + + action("B") { + ... + deps = [ ":A" ] # B inherits "a.in" as an implicit input + } + + action("C") { + ... + public_deps = [ ":A" ] # C inherits "a.in", and propagates it to + # C's dependents. + } +``` ### <a name="var_rebase"></a>**rebase**: Rebase collected metadata as files. [Back to Top](#gn-reference) ```
diff --git a/src/gn/action_target_generator.cc b/src/gn/action_target_generator.cc index 9cc9c83..4a7b2b7 100644 --- a/src/gn/action_target_generator.cc +++ b/src/gn/action_target_generator.cc
@@ -45,6 +45,9 @@ if (!FillInputs()) return; + if (!FillPublicInputs()) + return; + if (!FillScript()) return; @@ -260,3 +263,17 @@ target_->config_values().inputs().swap(dest_inputs); return true; } + +bool ActionTargetGenerator::FillPublicInputs() { + const Value* value = scope_->GetValue(variables::kPublicInputs, true); + if (!value) + return true; + + Target::FileList dest_public_inputs; + if (!ExtractListOfRelativeFiles(scope_->settings()->build_settings(), *value, + scope_->GetSourceDir(), &dest_public_inputs, + err_)) + return false; + target_->public_inputs().swap(dest_public_inputs); + return true; +}
diff --git a/src/gn/action_target_generator.h b/src/gn/action_target_generator.h index 6cbe8ae..61e11df 100644 --- a/src/gn/action_target_generator.h +++ b/src/gn/action_target_generator.h
@@ -29,6 +29,7 @@ bool FillMnemonic(); bool FillPool(); bool FillInputs(); + bool FillPublicInputs(); // Checks for errors in the outputs variable. bool CheckOutputs();
diff --git a/src/gn/ninja_binary_target_writer_unittest.cc b/src/gn/ninja_binary_target_writer_unittest.cc index e8a6f51..d8e12bc 100644 --- a/src/gn/ninja_binary_target_writer_unittest.cc +++ b/src/gn/ninja_binary_target_writer_unittest.cc
@@ -198,3 +198,55 @@ EXPECT_EQ(expected, out_str); } } + +TEST_F(NinjaBinaryTargetWriterTest, PublicInputs) { + Err err; + TestWithScope setup; + + // Action A has public_inputs. + Target a(setup.settings(), Label(SourceDir("//foo/"), "a")); + a.set_output_type(Target::ACTION); + a.visibility().SetPublic(); + a.SetToolchain(setup.toolchain()); + a.action_values().set_script(SourceFile("//foo/script.py")); + a.public_inputs().push_back(SourceFile("//foo/a.in")); + + // Source set B depends on A. + Target b(setup.settings(), Label(SourceDir("//foo/"), "b")); + b.set_output_type(Target::SOURCE_SET); + b.visibility().SetPublic(); + b.sources().push_back(SourceFile("//foo/source1.cc")); + b.source_types_used().Set(SourceFile::SOURCE_CPP); + b.SetToolchain(setup.toolchain()); + b.private_deps().push_back(LabelTargetPair(&a)); + + ASSERT_TRUE(a.OnResolved(&err)); + ASSERT_TRUE(b.OnResolved(&err)); + + std::ostringstream out; + NinjaBinaryTargetWriter writer(&b, out); + writer.Run(); + + // B's compile lines should depend on a.in (since it inherits it from A). + // Because it only has one input (a.in), it is depended on directly instead + // of via a phony target. + 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.source1.o: cxx ../../foo/source1.cc | " + "phony/foo/a.public_inputs || phony/foo/a\n" + " source_file_part = source1.cc\n" + " source_name_part = source1\n" + "\n" + "build phony/foo/b.linkdeps: phony obj/foo/b.source1.o || phony/foo/a\n" + "build phony/foo/b: phony phony/foo/b.linkdeps\n"; + std::string out_str = out.str(); + EXPECT_EQ(expected, out_str); +}
diff --git a/src/gn/ninja_outputs_writer_unittest.cc b/src/gn/ninja_outputs_writer_unittest.cc index 154d84b..8098d02 100644 --- a/src/gn/ninja_outputs_writer_unittest.cc +++ b/src/gn/ninja_outputs_writer_unittest.cc
@@ -11,6 +11,7 @@ #include "gn/builder_record.h" #include "gn/filesystem_utils.h" #include "gn/ninja_target_writer.h" +#include "gn/resolved_target_data.h" #include "gn/setup.h" #include "gn/switches.h" #include "gn/test_with_scheduler.h" @@ -33,7 +34,8 @@ // Called on worker thread to write the ninja file. void BackgroundDoWrite(TargetWriteInfo* write_info, const Target* target) { std::vector<OutputFile> target_ninja_outputs; - std::string rule = NinjaTargetWriter::RunAndWriteFile(target, nullptr, + ResolvedTargetData resolved; + std::string rule = NinjaTargetWriter::RunAndWriteFile(target, &resolved, &target_ninja_outputs); std::lock_guard<std::mutex> lock(write_info->lock);
diff --git a/src/gn/ninja_target_writer.cc b/src/gn/ninja_target_writer.cc index 6753701..abdbffc 100644 --- a/src/gn/ninja_target_writer.cc +++ b/src/gn/ninja_target_writer.cc
@@ -192,6 +192,8 @@ CHECK(0) << "Output type of target not handled."; } + WritePublicInputsStampOrPhony(target, resolved, rules); + if (needs_file_write) { // Write the ninja file. SourceFile ninja_file = GetNinjaFileForTarget(target); @@ -215,6 +217,45 @@ return storage.str(); } +// static +void NinjaTargetWriter::WritePublicInputsStampOrPhony( + const Target* target, + ResolvedTargetData* resolved, + std::ostream& out) { + DCHECK(resolved); + if (!resolved->ExportsPublicInputs(target)) + return; + + const BuildSettings* build_settings = target->settings()->build_settings(); + OutputFile output = GetPublicInputsOutputFile(target, build_settings); + + std::vector<OutputFile> deps; + for (const auto& file : target->public_inputs()) { + deps.push_back(OutputFile(build_settings, file)); + } + for (const auto& dep : target->public_deps()) { + if (resolved->ExportsPublicInputs(dep.ptr)) { + deps.push_back(GetPublicInputsOutputFile(dep.ptr, build_settings)); + } + } + + PathOutput path_output(build_settings->build_dir(), + build_settings->root_path_utf8(), ESCAPE_NINJA); + + out << "build "; + path_output.WriteFile(out, output); + + if (build_settings->no_stamp_files()) { + out << ": " << BuiltinTool::kBuiltinToolPhony; + } else { + out << ": " << GetNinjaRulePrefixForToolchain(target->settings()) + << GeneralTool::kGeneralToolStamp; + } + + path_output.WriteFiles(out, deps); + out << std::endl << std::endl; +} + void NinjaTargetWriter::WriteEscapedSubstitution(const Substitution* type) { EscapeOptions opts; opts.mode = ESCAPE_NINJA; @@ -538,6 +579,27 @@ return InputDeps{}; // No input dependencies. InputDeps deps; + // Inherited public_inputs target dependencies. + std::vector<OutputFile> public_inputs_deps; + for (const auto& pair : target_->public_deps()) { + if (resolved().ExportsPublicInputs(pair.ptr)) { + public_inputs_deps.push_back( + GetPublicInputsOutputFile(pair.ptr, settings_->build_settings())); + } + } + for (const auto& pair : target_->private_deps()) { + if (resolved().ExportsPublicInputs(pair.ptr)) { + public_inputs_deps.push_back( + GetPublicInputsOutputFile(pair.ptr, settings_->build_settings())); + } + } + std::sort(public_inputs_deps.begin(), public_inputs_deps.end()); + public_inputs_deps.erase( + std::unique(public_inputs_deps.begin(), public_inputs_deps.end()), + public_inputs_deps.end()); + deps.implicit.insert(deps.implicit.end(), public_inputs_deps.begin(), + public_inputs_deps.end()); + // File input deps. for (const SourceFile* source : input_deps_sources) deps.order_only.push_back(OutputFile(settings_->build_settings(), *source));
diff --git a/src/gn/ninja_target_writer.h b/src/gn/ninja_target_writer.h index 2da7e76..d7b2883 100644 --- a/src/gn/ninja_target_writer.h +++ b/src/gn/ninja_target_writer.h
@@ -54,6 +54,13 @@ ResolvedTargetData* resolved = nullptr, std::vector<OutputFile>* ninja_outputs = nullptr); + // Writes a stamp or phony rule for the target's public inputs if it exports + // any. This does nothing if resolved->ExportsPublicInputs(target) returns + // false. + static void WritePublicInputsStampOrPhony(const Target* target, + ResolvedTargetData* resolved, + std::ostream& out); + virtual void Run() = 0; protected:
diff --git a/src/gn/ninja_target_writer_unittest.cc b/src/gn/ninja_target_writer_unittest.cc index 9cdfaaa..c4fdbd8 100644 --- a/src/gn/ninja_target_writer_unittest.cc +++ b/src/gn/ninja_target_writer_unittest.cc
@@ -647,3 +647,53 @@ stream.str()); } } + +TEST(NinjaTargetWriter, PublicInputs) { + TestWithScope setup; + Err err; + + // Action A has public_inputs. + Target a(setup.settings(), Label(SourceDir("//foo/"), "a")); + a.set_output_type(Target::ACTION); + a.visibility().SetPublic(); + a.SetToolchain(setup.toolchain()); + a.action_values().set_script(SourceFile("//foo/script.py")); + a.public_inputs().push_back(SourceFile("//foo/a.in")); + + // Action B depends on A. + Target b(setup.settings(), Label(SourceDir("//foo/"), "b")); + b.set_output_type(Target::ACTION); + b.visibility().SetPublic(); + b.SetToolchain(setup.toolchain()); + b.action_values().set_script(SourceFile("//foo/script.py")); + b.private_deps().push_back(LabelTargetPair(&a)); + + ASSERT_TRUE(a.OnResolved(&err)); + ASSERT_TRUE(b.OnResolved(&err)); + + // 1. Verify A's public_inputs phony/stamp target is written. + { + std::ostringstream stream; + ResolvedTargetData resolved; + TestingNinjaTargetWriter::WritePublicInputsStampOrPhony(&a, &resolved, + stream); + EXPECT_EQ("build phony/foo/a.public_inputs: phony ../../foo/a.in\n\n", + stream.str()); + } + + // 2. Verify B's input deps. It should depend on A's public_inputs. + { + std::ostringstream stream; + TestingNinjaTargetWriter writer(&b, setup.toolchain(), stream); + auto dep = writer.WriteInputDepsStampOrPhonyAndGetDep( + std::vector<const Target*>(), 10u); + + ASSERT_EQ(1u, dep.implicit.size()); + EXPECT_EQ("phony/foo/b.inputdeps", dep.implicit[0].value()); + + std::string out = stream.str(); + EXPECT_TRUE(out.find("phony/foo/a.public_inputs") != std::string::npos) + << out; + EXPECT_TRUE(out.find("../../foo/a.in") == std::string::npos) << out; + } +}
diff --git a/src/gn/ninja_utils.cc b/src/gn/ninja_utils.cc index 95cf3a1..ae03c82 100644 --- a/src/gn/ninja_utils.cc +++ b/src/gn/ninja_utils.cc
@@ -4,7 +4,9 @@ #include "gn/ninja_utils.h" +#include "gn/build_settings.h" #include "gn/filesystem_utils.h" +#include "gn/output_file.h" #include "gn/settings.h" #include "gn/target.h" @@ -28,3 +30,18 @@ return std::string(); // Default toolchain has no prefix. return settings->toolchain_label().name() + "_"; } + +OutputFile GetPublicInputsOutputFile(const Target* target, + const BuildSettings* build_settings) { + OutputFile result; + if (build_settings->no_stamp_files()) { + result = GetBuildDirForTargetAsOutputFile(target, BuildDirType::PHONY); + result.append(target->label().name()); + result.append(".public_inputs"); + } else { + result = GetBuildDirForTargetAsOutputFile(target, BuildDirType::OBJ); + result.append(target->label().name()); + result.append(".public_inputs.stamp"); + } + return result; +}
diff --git a/src/gn/ninja_utils.h b/src/gn/ninja_utils.h index 60ae6b2..e629d91 100644 --- a/src/gn/ninja_utils.h +++ b/src/gn/ninja_utils.h
@@ -7,6 +7,8 @@ #include <string> +class BuildSettings; +class OutputFile; class Settings; class SourceFile; class Target; @@ -22,4 +24,9 @@ // don't collide with rules from other toolchains. std::string GetNinjaRulePrefixForToolchain(const Settings* settings); +// Returns the output file path for the target's public inputs stamp or phony +// target. +OutputFile GetPublicInputsOutputFile(const Target* target, + const BuildSettings* build_settings); + #endif // TOOLS_GN_NINJA_UTILS_H_
diff --git a/src/gn/resolved_target_data.cc b/src/gn/resolved_target_data.cc index bce9839..90063d9 100644 --- a/src/gn/resolved_target_data.cc +++ b/src/gn/resolved_target_data.cc
@@ -338,3 +338,20 @@ info->order_only_deps = all_order_only_deps.release(); } + +bool ResolvedTargetData::ComputeExportsPublicInputs( + const TargetInfo* info) const { + const Target* target = info->target; + + if (!target->public_inputs().empty()) { + return true; + } + + for (const auto& dep : target->public_deps()) { + if (ExportsPublicInputs(dep.ptr)) { + return true; + } + } + + return false; +}
diff --git a/src/gn/resolved_target_data.h b/src/gn/resolved_target_data.h index 0046481..e7c6e86 100644 --- a/src/gn/resolved_target_data.h +++ b/src/gn/resolved_target_data.h
@@ -152,7 +152,27 @@ return GetTargetOrderOnlyDeps(target)->order_only_deps; } + // Returns true if this target exports public inputs, either directly or from + // public dependencies which do. + bool ExportsPublicInputs(const Target* target) const { + TargetInfo* info = GetTargetInfo(target); + LazyBool value = + info->does_export_public_inputs.load(std::memory_order_acquire); + if (value == LazyBool::kUnknown) { + value = + ComputeExportsPublicInputs(info) ? LazyBool::kTrue : LazyBool::kFalse; + info->does_export_public_inputs.store(value, std::memory_order_release); + } + return value == LazyBool::kTrue; + } + private: + enum class LazyBool : uint8_t { + kUnknown, + kTrue, + kFalse, + }; + // The information associated with a given Target pointer. struct TargetInfo { TargetInfo() = default; @@ -175,6 +195,7 @@ std::atomic<bool> has_rust_libs = false; std::atomic<bool> has_swift_values = false; std::atomic<bool> has_order_only_deps = false; + std::atomic<LazyBool> does_export_public_inputs = LazyBool::kUnknown; // Only valid if |has_lib_info| is true. std::vector<SourceDir> lib_dirs; @@ -330,6 +351,7 @@ void ComputeRustLibs(TargetInfo* info) const; void ComputeSwiftValues(TargetInfo* info) const; void ComputeOrderOnlyDeps(TargetInfo* info) const; + bool ComputeExportsPublicInputs(const TargetInfo* info) const; // Helper function used by ComputeInheritedLibs(). void ComputeInheritedLibsFor(
diff --git a/src/gn/resolved_target_data_unittest.cc b/src/gn/resolved_target_data_unittest.cc index 92b965d..be3e950 100644 --- a/src/gn/resolved_target_data_unittest.cc +++ b/src/gn/resolved_target_data_unittest.cc
@@ -440,3 +440,53 @@ EXPECT_EQ(&b, a_module_deps[1].target()); EXPECT_EQ(&c, a_module_deps[2].target()); } + +TEST(ResolvedTargetDataTest, PublicInputsInheritance) { + TestWithScope setup; + Err err; + + // A has public_inputs. + TestTarget a(setup, "//foo:a", Target::ACTION); + a.public_inputs().push_back(SourceFile("//foo/a.in")); + + // B depends on A via private deps. + TestTarget b(setup, "//foo:b", Target::ACTION); + b.private_deps().push_back(LabelTargetPair(&a)); + + // C depends on A via public deps. + TestTarget c(setup, "//foo:c", Target::ACTION); + c.public_deps().push_back(LabelTargetPair(&a)); + + // D depends on C (which has A as public dep) via private deps. + TestTarget d(setup, "//foo:d", Target::ACTION); + d.private_deps().push_back(LabelTargetPair(&c)); + + // E depends on B (which has A as private dep) via private deps. + TestTarget e(setup, "//foo:e", Target::ACTION); + e.private_deps().push_back(LabelTargetPair(&b)); + + ASSERT_TRUE(a.OnResolved(&err)); + ASSERT_TRUE(b.OnResolved(&err)); + ASSERT_TRUE(c.OnResolved(&err)); + ASSERT_TRUE(d.OnResolved(&err)); + ASSERT_TRUE(e.OnResolved(&err)); + + ResolvedTargetData resolved; + + // A has public_inputs directly. + EXPECT_TRUE(resolved.ExportsPublicInputs(&a)); + + // B has A as a private dependency, so B itself does NOT export public_inputs + // for its dependents. + EXPECT_FALSE(resolved.ExportsPublicInputs(&b)); + + // C has A as a public dependency, so C exports public_inputs to propagate. + EXPECT_TRUE(resolved.ExportsPublicInputs(&c)); + + // D has C as a private dependency, so D itself does NOT export public_inputs + // for its dependents. + EXPECT_FALSE(resolved.ExportsPublicInputs(&d)); + + // E has B as a private dependency, B has no public inputs, so E has none. + EXPECT_FALSE(resolved.ExportsPublicInputs(&e)); +}
diff --git a/src/gn/target.h b/src/gn/target.h index 347ca6b..12cbef8 100644 --- a/src/gn/target.h +++ b/src/gn/target.h
@@ -166,6 +166,9 @@ const FileList& sources() const { return sources_; } FileList& sources() { return sources_; } + const FileList& public_inputs() const { return public_inputs_; } + FileList& public_inputs() { return public_inputs_; } + const SourceFileTypeSet& source_types_used() const { return source_types_used_; } @@ -558,6 +561,7 @@ FileList sources_; SourceFileTypeSet source_types_used_; + FileList public_inputs_; bool all_headers_public_ = true; FileList public_headers_; bool check_includes_ = true;
diff --git a/src/gn/variables.cc b/src/gn/variables.cc index dcb46e8..9e99985 100644 --- a/src/gn/variables.cc +++ b/src/gn/variables.cc
@@ -1393,6 +1393,47 @@ } )"; +const char kPublicInputs[] = "public_inputs"; +const char kPublicInputs_HelpShort[] = + "public_inputs: [file list] Declare public inputs."; +const char kPublicInputs_Help[] = + R"(public_inputs: Declare public inputs. + + Inputs to a target that should be treated as implicit inputs of any dependent + targets. + + If target A declares public_inputs and target B depends on A (either via + deps or public_deps), B will inherit A's public_inputs. + + If target B depends on A via public_deps, B's public_inputs will also + propagate to any targets depending on B. + + This is particularly useful for actions that generate source code which + contain implicit imports/includes of the files declared in public_inputs. + Dependent targets will automatically inherit these dependencies and trigger + rebuilds when the public inputs change. + + See also "inputs" and "public_deps". + + Example + + action("A") { + ... + public_inputs = [ "a.in" ] + } + + action("B") { + ... + deps = [ ":A" ] # B inherits "a.in" as an implicit input + } + + action("C") { + ... + public_deps = [ ":A" ] # C inherits "a.in", and propagates it to + # C's dependents. + } +)"; + const char kLdflags[] = "ldflags"; const char kLdflags_HelpShort[] = "ldflags: [string list] Flags passed to the linker."; @@ -2551,6 +2592,7 @@ INSERT_VARIABLE(Public) INSERT_VARIABLE(PublicConfigs) INSERT_VARIABLE(PublicDeps) + INSERT_VARIABLE(PublicInputs) INSERT_VARIABLE(Rebase) INSERT_VARIABLE(ResponseFileContents) INSERT_VARIABLE(Script)
diff --git a/src/gn/variables.h b/src/gn/variables.h index 66e0680..3ecb62a 100644 --- a/src/gn/variables.h +++ b/src/gn/variables.h
@@ -222,6 +222,10 @@ extern const char kInputs_HelpShort[]; extern const char kInputs_Help[]; +extern const char kPublicInputs[]; +extern const char kPublicInputs_HelpShort[]; +extern const char kPublicInputs_Help[]; + extern const char kLdflags[]; extern const char kLdflags_HelpShort[]; extern const char kLdflags_Help[];