Allow host_os and host_cpu to be accessed in .gn files

This exposes host_os and host_cpu in the dotfile evaluation scope
alongside gn_version.

This is derived from
https://chromium-review.git.corp.google.com/c/v8/v8/+/8285754/comment/69434b1d_b71efa64/

Bug: 551837554
Change-Id: Iaded5fbf5c03b00bfbc3f9915c73b34dc6e42b33
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/25700
Reviewed-by: Andrew Grieve <agrieve@google.com>
Commit-Queue: David Turner <digit@google.com>
Reviewed-by: David Turner <digit@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 719bc5d..41bd09f 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -4821,9 +4821,9 @@
 
 ```
   Corresponds to the number printed by `gn --version`. This variable is
-  only variable available in the dotfile (all the rest are missing
-  because the dotfile has to be parsed before args.gn or anything else
-  is processed).
+  one of the few variables available in the dotfile (along with `host_cpu`
+  and `host_os`, all the rest are missing because the dotfile has to be
+  parsed before args.gn or anything else is processed).
 ```
 
 #### **Example**
@@ -4835,7 +4835,7 @@
 
 ```
   This is value is exposed so that cross-compile toolchains can access the host
-  architecture when needed.
+  architecture when needed. It is also available in the dotfile.
 
   The value should generally be considered read-only, but it can be overridden
   in order to handle unusual cases where there might be multiple plausible
@@ -4853,7 +4853,7 @@
 
 ```
   This value is exposed so that cross-compiles can access the host build
-  system's settings.
+  system's settings. It is also available in the dotfile.
 
   This value should generally be treated as read-only. It, however, is not used
   internally by GN for any purpose.
@@ -7517,7 +7517,8 @@
 
   Next, project-specific overrides are applied. These are specified inside
   the default_args variable of //.gn. See "gn help dotfile" for more. Note
-  that during processing of the dotfile itself, only `gn_version` is defined.
+  that during processing of the dotfile itself, only `gn_version`, `host_cpu`,
+  and `host_os` are defined.
 
   If specified, arguments from the --args command line flag are used. If that
   flag is not specified, args from previous builds in the build directory will
@@ -7577,9 +7578,9 @@
 
     gn gen out/Debug --root=/home/build --dotfile=/home/my_gn_file.gn
 
-  The system variable `gn_version` is available in the dotfile, but none of
-  the other variables are, because the dotfile is processed before args.gn
-  or anything else is processed.
+  The system variables `gn_version`, `host_cpu`, and `host_os` are available
+  in the dotfile, but none of the other variables are, because the dotfile is
+  processed before args.gn or anything else is processed.
 ```
 
 #### **Variables**
diff --git a/src/gn/args.cc b/src/gn/args.cc
index a623f86..d78d862 100644
--- a/src/gn/args.cc
+++ b/src/gn/args.cc
@@ -31,7 +31,8 @@
 
   Next, project-specific overrides are applied. These are specified inside
   the default_args variable of //.gn. See "gn help dotfile" for more. Note
-  that during processing of the dotfile itself, only `gn_version` is defined.
+  that during processing of the dotfile itself, only `gn_version`, `host_cpu`,
+  and `host_os` are defined.
 
   If specified, arguments from the --args command line flag are used. If that
   flag is not specified, args from previous builds in the build directory will
@@ -335,35 +336,37 @@
   return result;
 }
 
-void Args::SetSystemVarsLocked(Scope* dest) const {
-  // Host OS.
-  const char* os = nullptr;
+// static
+const char* Args::GetHostOs() {
 #if defined(OS_WIN) || defined(OS_MSYS)
-  os = "win";
+  return "win";
 #elif defined(OS_MACOSX)
-  os = "mac";
+  return "mac";
 #elif defined(OS_LINUX)
-  os = "linux";
+  return "linux";
 #elif defined(OS_FREEBSD)
-  os = "freebsd";
+  return "freebsd";
 #elif defined(OS_AIX)
-  os = "aix";
+  return "aix";
 #elif defined(OS_OPENBSD)
-  os = "openbsd";
+  return "openbsd";
 #elif defined(OS_HAIKU)
-  os = "haiku";
+  return "haiku";
 #elif defined(OS_SOLARIS)
-  os = "solaris";
+  return "solaris";
 #elif defined(OS_NETBSD)
-  os = "netbsd";
+  return "netbsd";
 #elif defined(OS_ZOS)
-  os = "zos";
+  return "zos";
 #elif defined(OS_SERENITY)
-  os = "serenity";
+  return "serenity";
 #else
 #error Unknown OS type.
 #endif
+}
 
+// static
+const char* Args::GetHostCpu() {
   // Host architecture.
   static const char kX86[] = "x86";
   static const char kX64[] = "x64";
@@ -378,42 +381,47 @@
   static const char kRISCV64[] = "riscv64";
   static const char kE2K[] = "e2k";
   static const char kLOONG64[] = "loong64";
-  const char* arch = nullptr;
 
   // Set the host CPU architecture based on the underlying OS, not
   // whatever the current bit-tedness of the GN binary is.
   std::string os_arch = OperatingSystemArchitecture();
   if (os_arch == "x86" || os_arch == "BePC")
-    arch = kX86;
-  else if (os_arch == "x86_64")
-    arch = kX64;
-  else if (os_arch == "aarch64" || os_arch == "arm64")
-    arch = kArm64;
-  else if (os_arch.substr(0, 3) == "arm")
-    arch = kArm;
-  else if (os_arch == "mips")
-    arch = kMips;
-  else if (os_arch == "mips64")
-    arch = kMips64;
-  else if (os_arch == "s390x")
-    arch = kS390X;
-  else if (os_arch == "ppc64" || os_arch == "ppc64le")
+    return kX86;
+  if (os_arch == "x86_64")
+    return kX64;
+  if (os_arch == "aarch64" || os_arch == "arm64")
+    return kArm64;
+  if (os_arch.substr(0, 3) == "arm")
+    return kArm;
+  if (os_arch == "mips")
+    return kMips;
+  if (os_arch == "mips64")
+    return kMips64;
+  if (os_arch == "s390x")
+    return kS390X;
+  if (os_arch == "ppc64" || os_arch == "ppc64le")
     // We handle the endianness inside //build/config/host_byteorder.gni.
     // This allows us to use the same toolchain as ppc64 BE
     // and specific flags are included using the host_byteorder logic.
-    arch = kPPC64;
-  else if (os_arch == "sparc64")
-    arch = kSPARC64;
-  else if (os_arch == "riscv32")
-    arch = kRISCV32;
-  else if (os_arch == "riscv64")
-    arch = kRISCV64;
-  else if (os_arch == "e2k")
-    arch = kE2K;
-  else if (os_arch == "loongarch64")
-    arch = kLOONG64;
-  else
-    CHECK(false) << "OS architecture not handled. (" << os_arch << ")";
+    return kPPC64;
+  if (os_arch == "sparc64")
+    return kSPARC64;
+  if (os_arch == "riscv32")
+    return kRISCV32;
+  if (os_arch == "riscv64")
+    return kRISCV64;
+  if (os_arch == "e2k")
+    return kE2K;
+  if (os_arch == "loongarch64")
+    return kLOONG64;
+
+  CHECK(false) << "OS architecture not handled. (" << os_arch << ")";
+  return nullptr;
+}
+
+void Args::SetSystemVarsLocked(Scope* dest) const {
+  const char* os = GetHostOs();
+  const char* arch = GetHostCpu();
 
   // Save the OS and architecture as build arguments that are implicitly
   // declared. This is so they can be overridden in a toolchain build args
diff --git a/src/gn/args.h b/src/gn/args.h
index 0bc1821..07ae332 100644
--- a/src/gn/args.h
+++ b/src/gn/args.h
@@ -61,6 +61,12 @@
   // has an override, it returns `override_value`.
   std::optional<Value> GetArgFromAllArguments(const char* name) const;
 
+  // Returns the default host_os string based on the current platform.
+  static const char* GetHostOs();
+
+  // Returns the default host_cpu string based on the current platform.
+  static const char* GetHostCpu();
+
   // Sets up the root scope for a toolchain. This applies the default system
   // flags and saves the toolchain overrides so they can be applied to
   // declare_args blocks that appear when loading files in that toolchain.
diff --git a/src/gn/scope_per_file_provider.cc b/src/gn/scope_per_file_provider.cc
index fa7a1d0..848c2a1 100644
--- a/src/gn/scope_per_file_provider.cc
+++ b/src/gn/scope_per_file_provider.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "gn/args.h"
 #include "gn/filesystem_utils.h"
 #include "gn/settings.h"
 #include "gn/source_file.h"
@@ -28,9 +29,17 @@
   if (ident == variables::kGnVersion)
     return GetGnVersion();
 
-  // In the dotfile scope, gn_version is the only thing defined.
-  if (dotfile_scope_)
+  // In the dotfile scope, only gn_version, host_cpu, and host_os are defined.
+  // host_cpu and host_os are only provided here for the dotfile scope because
+  // in normal scopes they are build arguments that can be overridden (which
+  // would otherwise be shadowed by programmatic values).
+  if (dotfile_scope_) {
+    if (ident == variables::kHostCpu)
+      return GetHostCpu();
+    if (ident == variables::kHostOs)
+      return GetHostOs();
     return nullptr;
+  }
 
   if (ident == variables::kCurrentToolchain)
     return GetCurrentToolchain();
@@ -82,6 +91,20 @@
   return gn_version_.get();
 }
 
+const Value* ScopePerFileProvider::GetHostCpu() {
+  if (!host_cpu_) {
+    host_cpu_ = std::make_unique<Value>(nullptr, Args::GetHostCpu());
+  }
+  return host_cpu_.get();
+}
+
+const Value* ScopePerFileProvider::GetHostOs() {
+  if (!host_os_) {
+    host_os_ = std::make_unique<Value>(nullptr, Args::GetHostOs());
+  }
+  return host_os_.get();
+}
+
 const Value* ScopePerFileProvider::GetPythonPath() {
   if (!python_path_) {
     python_path_ = std::make_unique<Value>(
diff --git a/src/gn/scope_per_file_provider.h b/src/gn/scope_per_file_provider.h
index 9f7adb0..4c0711f 100644
--- a/src/gn/scope_per_file_provider.h
+++ b/src/gn/scope_per_file_provider.h
@@ -18,7 +18,7 @@
   // When allow_target_vars is unset, the target-related values will be
   // undefined to GN script. When dotfile_scope is set, only the values
   // safe to reference in a dotfile will be resolved. At the moment that
-  // is just gn_version.
+  // is gn_version, host_cpu, and host_os.
   ScopePerFileProvider(Scope* scope,
                        bool allow_target_vars,
                        bool dotfile_scope = false);
@@ -31,6 +31,8 @@
   const Value* GetCurrentToolchain();
   const Value* GetDefaultToolchain();
   const Value* GetGnVersion();
+  const Value* GetHostCpu();
+  const Value* GetHostOs();
   const Value* GetPythonPath();
   const Value* GetRootBuildDir();
   const Value* GetRootGenDir();
@@ -45,6 +47,8 @@
   std::unique_ptr<Value> current_toolchain_;
   std::unique_ptr<Value> default_toolchain_;
   std::unique_ptr<Value> gn_version_;
+  std::unique_ptr<Value> host_cpu_;
+  std::unique_ptr<Value> host_os_;
   std::unique_ptr<Value> python_path_;
   std::unique_ptr<Value> root_build_dir_;
   std::unique_ptr<Value> root_gen_dir_;
diff --git a/src/gn/scope_per_file_provider_unittest.cc b/src/gn/scope_per_file_provider_unittest.cc
index d4a458f..18f610e 100644
--- a/src/gn/scope_per_file_provider_unittest.cc
+++ b/src/gn/scope_per_file_provider_unittest.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "gn/scope_per_file_provider.h"
+#include "gn/args.h"
 #include "gn/build_settings.h"
 #include "gn/settings.h"
 #include "gn/test_with_scope.h"
@@ -62,6 +63,14 @@
     ScopePerFileProvider provider(&scope, false, true);
     EXPECT_GE(provider.GetProgrammaticValue(variables::kGnVersion)->int_value(),
               0);
+    EXPECT_NE(nullptr, provider.GetProgrammaticValue(variables::kHostCpu));
+    EXPECT_NE(nullptr, provider.GetProgrammaticValue(variables::kHostOs));
+    EXPECT_EQ(
+        Args::GetHostCpu(),
+        provider.GetProgrammaticValue(variables::kHostCpu)->string_value());
+    EXPECT_EQ(
+        Args::GetHostOs(),
+        provider.GetProgrammaticValue(variables::kHostOs)->string_value());
     EXPECT_EQ(nullptr, provider.GetProgrammaticValue(variables::kRootBuildDir));
   }
 }
diff --git a/src/gn/setup.cc b/src/gn/setup.cc
index 47b0282..eb946eb 100644
--- a/src/gn/setup.cc
+++ b/src/gn/setup.cc
@@ -60,9 +60,9 @@
 
     gn gen out/Debug --root=/home/build --dotfile=/home/my_gn_file.gn
 
-  The system variable `gn_version` is available in the dotfile, but none of
-  the other variables are, because the dotfile is processed before args.gn
-  or anything else is processed.
+  The system variables `gn_version`, `host_cpu`, and `host_os` are available
+  in the dotfile, but none of the other variables are, because the dotfile is
+  processed before args.gn or anything else is processed.
 
 Variables
 
diff --git a/src/gn/setup_unittest.cc b/src/gn/setup_unittest.cc
index 0fe9388..0eee1e7 100644
--- a/src/gn/setup_unittest.cc
+++ b/src/gn/setup_unittest.cc
@@ -583,3 +583,36 @@
       base::FilePath(FILE_PATH_LITERAL("../../third_party/python/bin/python3"))
           .NormalizePathSeparatorsTo('/'));
 }
+
+TEST_F(SetupTest, HostOsAndHostCpuInDotFile) {
+  base::CommandLine cmdline(base::CommandLine::NO_PROGRAM);
+
+  const char kDotfileContents[] = R"(
+buildconfig = "//BUILDCONFIG.gn"
+assert(host_os != "")
+assert(host_cpu != "")
+if (host_os != "nonexistent_os") {
+  default_args = {
+    test_arg = host_os
+  }
+}
+)";
+
+  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_FALSE(err.has_error());
+}
diff --git a/src/gn/variables.cc b/src/gn/variables.cc
index a1ec0ba..3564369 100644
--- a/src/gn/variables.cc
+++ b/src/gn/variables.cc
@@ -17,9 +17,9 @@
     R"(gn_version: [number] The version of gn.
 
   Corresponds to the number printed by `gn --version`. This variable is
-  only variable available in the dotfile (all the rest are missing
-  because the dotfile has to be parsed before args.gn or anything else
-  is processed).
+  one of the few variables available in the dotfile (along with `host_cpu`
+  and `host_os`, all the rest are missing because the dotfile has to be
+  parsed before args.gn or anything else is processed).
 
 Example
 
@@ -33,7 +33,7 @@
     R"(host_cpu: The processor architecture that GN is running on.
 
   This is value is exposed so that cross-compile toolchains can access the host
-  architecture when needed.
+  architecture when needed. It is also available in the dotfile.
 
   The value should generally be considered read-only, but it can be overridden
   in order to handle unusual cases where there might be multiple plausible
@@ -53,7 +53,7 @@
     R"(host_os: [string] The operating system that GN is running on.
 
   This value is exposed so that cross-compiles can access the host build
-  system's settings.
+  system's settings. It is also available in the dotfile.
 
   This value should generally be treated as read-only. It, however, is not used
   internally by GN for any purpose.