Add `load` function to GN

Bug: 528225104
Change-Id: I52784a4173e6b4ee6ee675255b95ee9e6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/24801
Commit-Queue: Matt Stark <msta@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/build/gen.py b/build/gen.py
index 4dd28e6..80ca7e9 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -1153,6 +1153,7 @@
         'src/gn/ffi/session_unittest.cc',
     ])
     executables['gn_unittests']['libs'].append('gn_starlark')
+    executables['gn']['libs'].append('gn_starlark')
 
   # Write the absolute path of the source root to a file in the output directory
   # so that tests can locate the source tree robustly.
diff --git a/docs/reference.md b/docs/reference.md
index 62eb514..8247c48 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -56,6 +56,7 @@
     *   [import: Import a file into the current scope.](#func_import)
     *   [label_matches: Returns whether a label matches any of a list of patterns.](#func_label_matches)
     *   [len: Returns the length of a string or a list.](#func_len)
+    *   [load: Load variables from a starlark file into the current scope.](#func_load)
     *   [not_needed: Mark variables from scope as not needed.](#func_not_needed)
     *   [path_exists: Returns whether the given path exists.](#func_path_exists)
     *   [pool: Defines a pool object.](#func_pool)
@@ -3138,6 +3139,34 @@
   len("foo")  # 3
   len([ "a", "b", "c" ])  # 3
 ```
+### <a name="func_load"></a>**load**: Load variables from a starlark file into the current scope.&nbsp;[Back to Top](#gn-reference)
+
+```
+  The load command executes a Starlark (.scl) file in a standalone environment
+  and imports the specified symbols into the current scope.
+```
+
+#### **Arguments**
+
+```
+  First argument:
+    A label to the Starlark (.scl) file to load. This label can be
+    absolute (e.g. "//path/to:rules.scl") or relative to the current directory
+    (e.g, ":rules.scl")
+
+  Remaining arguments:
+    A list of string names of the symbols to load from the file.
+```
+
+#### **Example**:
+
+```
+  load("//:rules.scl", "custom_rule", "MY_CONSTANT")
+
+  custom_rule("a") {
+    foo = MY_CONSTANT
+  }
+```
 ### <a name="func_not_needed"></a>**not_needed**: Mark variables from scope as not needed.&nbsp;[Back to Top](#gn-reference)
 
 ```
diff --git a/src/gn/build_settings.cc b/src/gn/build_settings.cc
index 6d66376..359a5a7 100644
--- a/src/gn/build_settings.cc
+++ b/src/gn/build_settings.cc
@@ -7,6 +7,7 @@
 #include <utility>
 
 #include "base/files/file_util.h"
+#include "gn/ffi/bridge.h"
 #include "gn/filesystem_utils.h"
 
 BuildSettings::BuildSettings() = default;
@@ -23,7 +24,12 @@
       build_config_file_(other.build_config_file_),
       arg_file_template_path_(other.arg_file_template_path_),
       build_dir_(other.build_dir_),
-      build_args_(other.build_args_) {}
+      build_args_(other.build_args_) {
+  if (other.starlark_session_.has_value()) {
+    starlark_session_ =
+        Session::new_cxx(other.root_path_utf8_, other.build_dir_.value());
+  }
+}
 
 void BuildSettings::SetRootTargetLabel(const Label& r) {
   root_target_label_ = r;
@@ -37,6 +43,9 @@
   DCHECK(r.value()[r.value().size() - 1] != base::FilePath::kSeparators[0]);
   root_path_ = r.NormalizePathSeparatorsTo('/');
   root_path_utf8_ = FilePathToUTF8(root_path_);
+  if (!root_path_.empty() && !build_dir_.is_null()) {
+    starlark_session_ = Session::new_cxx(root_path_utf8_, build_dir_.value());
+  }
 }
 
 void BuildSettings::SetSecondarySourcePath(const SourceDir& d) {
@@ -59,6 +68,9 @@
 
 void BuildSettings::SetBuildDir(const SourceDir& d) {
   build_dir_ = d;
+  if (!root_path_.empty() && !build_dir_.is_null()) {
+    starlark_session_ = Session::new_cxx(root_path_utf8_, build_dir_.value());
+  }
 }
 
 base::FilePath BuildSettings::GetFullPath(const SourceFile& file) const {
@@ -100,4 +112,9 @@
   auto temp = std::move(print_callback_);
   print_callback_ = callback;
   return temp;
+}
+
+const Session& BuildSettings::starlark_session() const {
+  DCHECK(starlark_session_.has_value());
+  return **starlark_session_;
 }
\ No newline at end of file
diff --git a/src/gn/build_settings.h b/src/gn/build_settings.h
index 0d4decb..c5c5808 100644
--- a/src/gn/build_settings.h
+++ b/src/gn/build_settings.h
@@ -8,10 +8,12 @@
 #include <functional>
 #include <map>
 #include <memory>
+#include <optional>
 #include <set>
 #include <utility>
 
 #include "base/files/file_path.h"
+#include "cxx.h"
 #include "gn/args.h"
 #include "gn/label.h"
 #include "gn/label_pattern.h"
@@ -21,6 +23,7 @@
 #include "gn/version.h"
 
 class Item;
+struct Session;
 
 // Settings for one build, which is one toplevel output directory. There
 // may be multiple Settings objects that refer to this, one for each toolchain.
@@ -153,6 +156,8 @@
     expand_directory_allowlist_ = std::move(list);
   }
 
+  const Session& starlark_session() const;
+
  private:
   Label root_target_label_;
   std::vector<LabelPattern> root_patterns_;
@@ -179,6 +184,8 @@
   std::unique_ptr<SourceFileSet> expand_directory_allowlist_ =
       std::make_unique<SourceFileSet>();
 
+  std::optional<rust::Box<Session>> starlark_session_;
+
   BuildSettings& operator=(const BuildSettings&) = delete;
 };
 
diff --git a/src/gn/functions.cc b/src/gn/functions.cc
index 4650362..10b41ce 100644
--- a/src/gn/functions.cc
+++ b/src/gn/functions.cc
@@ -17,6 +17,8 @@
 #include "gn/config.h"
 #include "gn/config_values_generator.h"
 #include "gn/err.h"
+#include "gn/ffi/bridge.h"
+#include "gn/ffi/session.h"
 #include "gn/input_file.h"
 #include "gn/parse_node_value_adapter.h"
 #include "gn/parse_tree.h"
@@ -693,6 +695,68 @@
   return Value();
 }
 
+// load -----------------------------------------------------------------------
+
+const char kLoad[] = "load";
+const char kLoad_HelpShort[] =
+    "load: Load variables from a starlark file into the current scope.";
+const char kLoad_Help[] =
+    R"(load: Load variables from a starlark file into the current scope.
+
+  The load command executes a Starlark (.scl) file in a standalone environment
+  and imports the specified symbols into the current scope.
+
+Arguments
+
+  First argument:
+    A label to the Starlark (.scl) file to load. This label can be
+    absolute (e.g. "//path/to:rules.scl") or relative to the current directory
+    (e.g, ":rules.scl")
+
+  Remaining arguments:
+    A list of string names of the symbols to load from the file.
+
+Example:
+
+  load("//:rules.scl", "custom_rule", "MY_CONSTANT")
+
+  custom_rule("a") {
+    foo = MY_CONSTANT
+  }
+)";
+
+Value RunLoad(Scope* scope,
+              const FunctionCallNode* function,
+              const ListNode* args_list,
+              Err* err) {
+  const std::vector<std::unique_ptr<const ParseNode>>& args =
+      args_list->contents();
+  if (args.size() < 2) {
+    *err = Err(function->function(), "Incorrect arguments.",
+               "This function requires at least a file to import and a list of "
+               "variables to load.");
+    return Value();
+  }
+
+  std::vector<Value> values;
+  values.reserve(args.size());
+  for (const auto& arg : args) {
+    Value val = arg->Execute(scope, err);
+    if (err->has_error()) {
+      return Value();
+    }
+    values.push_back(std::move(val));
+  }
+
+  const ::Session& loader =
+      scope->settings()->build_settings()->starlark_session();
+
+  session_load(loader, values[0], std::span(values).subspan(1), *scope,
+               ParseNodePtr{function}, *err);
+
+  return Value();
+}
+
 // not_needed -----------------------------------------------------------------
 
 const char kNotNeeded[] = "not_needed";
@@ -1536,6 +1600,7 @@
     INSERT_FUNCTION(GetPathInfo, false)
     INSERT_FUNCTION(GetTargetOutputs, false)
     INSERT_FUNCTION(Import, false)
+    INSERT_FUNCTION(Load, false)
     INSERT_FUNCTION(LabelMatches, false)
     INSERT_FUNCTION(Len, false)
     INSERT_FUNCTION(NotNeeded, false)
diff --git a/src/gn/functions.h b/src/gn/functions.h
index bf97b9e..f3b48e7 100644
--- a/src/gn/functions.h
+++ b/src/gn/functions.h
@@ -260,6 +260,14 @@
                 const std::vector<Value>& args,
                 Err* err);
 
+extern const char kLoad[];
+extern const char kLoad_HelpShort[];
+extern const char kLoad_Help[];
+Value RunLoad(Scope* scope,
+              const FunctionCallNode* function,
+              const ListNode* args_list,
+              Err* err);
+
 extern const char kLabelMatches[];
 extern const char kLabelMatches_HelpShort[];
 extern const char kLabelMatches_Help[];
diff --git a/src/gn/functions_unittest.cc b/src/gn/functions_unittest.cc
index 6fc0e2f..1b7da65 100644
--- a/src/gn/functions_unittest.cc
+++ b/src/gn/functions_unittest.cc
@@ -7,6 +7,10 @@
 #include <memory>
 #include <utility>
 
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "gn/filesystem_utils.h"
 #include "gn/parse_tree.h"
 #include "gn/test_with_scope.h"
 #include "gn/value.h"
@@ -709,3 +713,56 @@
       "  print_stack_trace()  //test:6\n",
       setup.print_output());
 }
+
+TEST(Functions, Load) {
+  TestWithScope setup;
+  setup.build_settings()->SetRootPath(UTF8ToFilePath("."));
+  setup.scope()->set_source_dir(SourceDir("//"));
+
+  // Verify that .bzl files return an error as they are not supported.
+  {
+    TestParseInput input(R"gn(load("//:wrong_extension.bzl", "a"))gn");
+    ASSERT_SUCCESS(input);
+    Err err;
+    input.parsed()->Execute(setup.scope(), &err);
+    ASSERT_TRUE(err.has_error());
+    ASSERT_EQ(err.message(), "The file to load must be a '.scl' file.");
+  }
+
+  // Verify that an error is returned if the file doesn't exist.
+  {
+    TestParseInput input(R"gn(load("//:non_existent.scl", "a"))gn");
+    ASSERT_SUCCESS(input);
+    Err err;
+    input.parsed()->Execute(setup.scope(), &err);
+    ASSERT_TRUE(err.has_error());
+    ASSERT_EQ(err.message(), "Failed to read file: //:non_existent.scl");
+  }
+
+  // Verify that we can successfully load variables from a starlark file.
+  {
+    base::ScopedTempDir temp_dir;
+    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+    setup.build_settings()->SetRootPath(temp_dir.GetPath());
+    setup.scope()->set_source_dir(SourceDir("//"));
+
+    std::string scl_content = R"scl(
+a = "hello"
+)scl";
+    base::FilePath scl_path = temp_dir.GetPath().AppendASCII("rules.scl");
+    ASSERT_EQ(
+        static_cast<int>(scl_content.size()),
+        base::WriteFile(scl_path, scl_content.c_str(), scl_content.size()));
+
+    TestParseInput input(R"gn(load("//:rules.scl", "a"))gn");
+    ASSERT_SUCCESS(input);
+    Err err;
+    input.parsed()->Execute(setup.scope(), &err);
+    ASSERT_FALSE(err.has_error()) << err.message();
+
+    const Value* val_a = setup.scope()->GetValue("a");
+    ASSERT_TRUE(val_a);
+    EXPECT_EQ(Value::STRING, val_a->type());
+    EXPECT_EQ("hello", val_a->string_value());
+  }
+}