Support source-relative paths for script_executable

This is derived from
https://crrev.com/c/8160801/comment/bfc1cbdb_33949900/

Support source-relative paths starting with "//" for script_executable
in the dotfile (.gn) and via the --script-executable command line flag.

When specified with "//", the path is resolved relative to the source
root and subsequently converted into a build-dir-relative path in
BuildSettings::SetPythonPath. This avoids hardcoding relative paths from
the build directory.

Bug: 537680330
Change-Id: I64392fd11f021ec093d6aa3f9a3eb63d0c7223fd
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/24500
Reviewed-by: Sylvain Defresne <sdefresne@chromium.org>
Commit-Queue: Takuto Ikuta <tikuta@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index e4c9013..62eb514 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -7583,6 +7583,10 @@
       calls using the Python interpreter found in PATH. This value specifies the
       Python executable or other interpreter to use instead.
 
+      If set to a path starting with "//", it will be interpreted as relative
+      to the source root directory and converted to a build-directory-relative
+      path in the generated Ninja files.
+
       If set to the empty string, the scripts will be executed directly.
 
       The command-line switch --script-executable will override this value (see
diff --git a/src/gn/setup.cc b/src/gn/setup.cc
index 132d711..95e4c19 100644
--- a/src/gn/setup.cc
+++ b/src/gn/setup.cc
@@ -191,6 +191,10 @@
       calls using the Python interpreter found in PATH. This value specifies the
       Python executable or other interpreter to use instead.
 
+      If set to a path starting with "//", it will be interpreted as relative
+      to the source root directory and converted to a build-directory-relative
+      path in the generated Ninja files.
+
       If set to the empty string, the scripts will be executed directly.
 
       The command-line switch --script-executable will override this value (see
@@ -868,7 +872,15 @@
   if (cmdline.HasSwitch(switches::kScriptExecutable)) {
     auto script_executable =
         cmdline.GetSwitchValuePath(switches::kScriptExecutable);
-    build_settings_.SetPythonPath(ProcessFileExtensions(script_executable));
+    std::string str_val = FilePathToUTF8(script_executable);
+    base::FilePath python_path;
+    if (str_val.starts_with("//")) {
+      SourceFile source_file(std::move(str_val));
+      python_path = build_settings_.GetFullPath(source_file);
+    } else {
+      python_path = std::move(script_executable);
+    }
+    build_settings_.SetPythonPath(ProcessFileExtensions(python_path));
   } else if (value) {
     if (!value->VerifyTypeIs(Value::STRING, err)) {
       return false;
@@ -877,8 +889,14 @@
     // invoked by actions will be run directly.
     base::FilePath python_path;
     if (!value->string_value().empty()) {
-      python_path =
-          ProcessFileExtensions(UTF8ToFilePath(value->string_value()));
+      const std::string& str_val = value->string_value();
+      if (str_val.starts_with("//")) {
+        SourceFile source_file(str_val);
+        python_path = build_settings_.GetFullPath(source_file);
+      } else {
+        python_path = UTF8ToFilePath(str_val);
+      }
+      python_path = ProcessFileExtensions(python_path);
       if (python_path.empty()) {
         *err = Err(Location(), "Could not find \"" + value->string_value() +
                                    "\" from dotfile in PATH.");
diff --git a/src/gn/setup_unittest.cc b/src/gn/setup_unittest.cc
index 3140e52..0fe9388 100644
--- a/src/gn/setup_unittest.cc
+++ b/src/gn/setup_unittest.cc
@@ -552,3 +552,34 @@
   EXPECT_EQ(setup.build_settings().python_path(), script_executable);
 #endif
 }
+
+TEST_F(SetupTest, SourceRelativeScriptExecutable) {
+  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+
+  const char kDotfileContents[] = R"(
+buildconfig = "//BUILDCONFIG.gn"
+script_executable = "//third_party/python/bin/python3"
+)";
+
+  base::ScopedTempDir in_temp_dir;
+  ASSERT_TRUE(in_temp_dir.CreateUniqueTempDir());
+  base::FilePath in_path = base::MakeAbsoluteFilePath(in_temp_dir.GetPath());
+  base::FilePath dot_gn_name = in_path.Append(FILE_PATH_LITERAL(".gn"));
+  WriteFile(dot_gn_name, kDotfileContents);
+
+  WriteFile(in_path.Append(FILE_PATH_LITERAL("BUILDCONFIG.gn")), "");
+  cmdline.AppendSwitchPath(switches::kRoot, in_path);
+
+  base::FilePath build_dir = in_path.Append(FILE_PATH_LITERAL("out"))
+                                 .Append(FILE_PATH_LITERAL("default"));
+
+  Setup setup;
+  Err err;
+  EXPECT_TRUE(
+      setup.DoSetupWithErr(FilePathToUTF8(build_dir), true, cmdline, &err));
+  EXPECT_TRUE(setup.build_settings().python_path_is_relative_to_build_dir());
+  EXPECT_EQ(
+      setup.build_settings().python_path(),
+      base::FilePath(FILE_PATH_LITERAL("../../third_party/python/bin/python3"))
+          .NormalizePathSeparatorsTo('/'));
+}
diff --git a/src/gn/switches.cc b/src/gn/switches.cc
index 61b3425..27f30fd 100644
--- a/src/gn/switches.cc
+++ b/src/gn/switches.cc
@@ -153,6 +153,10 @@
   action targets and exec_script calls. By default GN searches the
   PATH for Python to execute these scripts.
 
+  If set to a path starting with "//", it will be interpreted as relative
+  to the source root directory and converted to a build-directory-relative
+  path in the generated Ninja files.
+
   If set to the empty string, the path of scripts specified in action
   targets and exec_script calls will be executed directly.
 )";