Make `gn_version` available in the dotfile.

It would be useful if `gn_version` was available in the dotfile
so that you can check for a specific version there before anything
else happens (the dotfile is processed before args.gn, BUILDCONFIG,
or any BUILD.gn file).

This CL restructures things to make that happen.

Note that it appears that this is the only variable that is really
safe to be defined in the dotfile and args.gn scopes, unfortunately.
It seems like it would be useful if at least `host_os` and `host_cpu`
were also available, but since those values can be modified via
`default_args` in the dotfile, it would be hard to reason about the
values for them.

Change-Id: Ia3c98e0288829e9f618e82591506ca805684618c
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/18521
Reviewed-by: David Turner <digit@google.com>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: David Turner <digit@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 36ccd6e..a11c362 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -4557,7 +4557,10 @@
 ### <a name="var_gn_version"></a>**gn_version**: [number] The version of gn.&nbsp;[Back to Top](#gn-reference)
 
 ```
-  Corresponds to the number printed by `gn --version`.
+  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).
 ```
 
 #### **Example**
@@ -7103,6 +7106,7 @@
 ```
   First, system default arguments are set based on the current system. The
   built-in arguments are:
+   - gn_version
    - host_cpu
    - host_os
    - current_cpu
@@ -7111,7 +7115,8 @@
    - target_os
 
   Next, project-specific overrides are applied. These are specified inside
-  the default_args variable of //.gn. See "gn help dotfile" for more.
+  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.
 
   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
@@ -7170,6 +7175,10 @@
   --dotfile:
 
     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.
 ```
 
 #### **Variables**
diff --git a/src/gn/args.cc b/src/gn/args.cc
index 6b5d25f..c67f8bc 100644
--- a/src/gn/args.cc
+++ b/src/gn/args.cc
@@ -21,6 +21,7 @@
 
   First, system default arguments are set based on the current system. The
   built-in arguments are:
+   - gn_version
    - host_cpu
    - host_os
    - current_cpu
@@ -29,7 +30,8 @@
    - target_os
 
   Next, project-specific overrides are applied. These are specified inside
-  the default_args variable of //.gn. See "gn help dotfile" for more.
+  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.
 
   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
diff --git a/src/gn/scope_per_file_provider.cc b/src/gn/scope_per_file_provider.cc
index 9332c40..8d706b6 100644
--- a/src/gn/scope_per_file_provider.cc
+++ b/src/gn/scope_per_file_provider.cc
@@ -14,19 +14,28 @@
 
 #include "last_commit_position.h"
 
-ScopePerFileProvider::ScopePerFileProvider(Scope* scope, bool allow_target_vars)
-    : ProgrammaticProvider(scope), allow_target_vars_(allow_target_vars) {}
+ScopePerFileProvider::ScopePerFileProvider(Scope* scope,
+                                           bool allow_target_vars,
+                                           bool dotfile_scope)
+    : ProgrammaticProvider(scope),
+      allow_target_vars_(allow_target_vars),
+      dotfile_scope_(dotfile_scope) {}
 
 ScopePerFileProvider::~ScopePerFileProvider() = default;
 
 const Value* ScopePerFileProvider::GetProgrammaticValue(
     std::string_view ident) {
+  if (ident == variables::kGnVersion)
+    return GetGnVersion();
+
+  // In the dotfile scope, gn_version is the only thing defined.
+  if (dotfile_scope_)
+    return nullptr;
+
   if (ident == variables::kCurrentToolchain)
     return GetCurrentToolchain();
   if (ident == variables::kDefaultToolchain)
     return GetDefaultToolchain();
-  if (ident == variables::kGnVersion)
-    return GetGnVersion();
   if (ident == variables::kPythonPath)
     return GetPythonPath();
 
diff --git a/src/gn/scope_per_file_provider.h b/src/gn/scope_per_file_provider.h
index 604e22f..9f7adb0 100644
--- a/src/gn/scope_per_file_provider.h
+++ b/src/gn/scope_per_file_provider.h
@@ -16,8 +16,12 @@
  public:
   // allow_target_vars allows the target-related variables to get resolved.
   // When allow_target_vars is unset, the target-related values will be
-  // undefined to GN script.
-  ScopePerFileProvider(Scope* scope, bool allow_target_vars);
+  // 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.
+  ScopePerFileProvider(Scope* scope,
+                       bool allow_target_vars,
+                       bool dotfile_scope = false);
   ~ScopePerFileProvider() override;
 
   // ProgrammaticProvider implementation.
@@ -35,6 +39,7 @@
   const Value* GetTargetOutDir();
 
   bool allow_target_vars_;
+  bool dotfile_scope_;
 
   // All values are lazily created.
   std::unique_ptr<Value> current_toolchain_;
diff --git a/src/gn/scope_per_file_provider_unittest.cc b/src/gn/scope_per_file_provider_unittest.cc
index 8e38903..d4a458f 100644
--- a/src/gn/scope_per_file_provider_unittest.cc
+++ b/src/gn/scope_per_file_provider_unittest.cc
@@ -55,4 +55,13 @@
     EXPECT_EQ("//out/Debug/tc/gen/source", GPV(variables::kTargetGenDir));
     EXPECT_EQ("//out/Debug/tc/obj/source", GPV(variables::kTargetOutDir));
   }
+
+  // Test just the dotfile scope.
+  {
+    Scope scope(test.settings());
+    ScopePerFileProvider provider(&scope, false, true);
+    EXPECT_GE(provider.GetProgrammaticValue(variables::kGnVersion)->int_value(),
+              0);
+    EXPECT_EQ(nullptr, provider.GetProgrammaticValue(variables::kRootBuildDir));
+  }
 }
diff --git a/src/gn/setup.cc b/src/gn/setup.cc
index f9c6dfe..0232089 100644
--- a/src/gn/setup.cc
+++ b/src/gn/setup.cc
@@ -59,6 +59,10 @@
 
     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.
+
 Variables
 
   arg_file_template [optional]
@@ -403,6 +407,9 @@
       dotfile_scope_(&dotfile_settings_) {
   dotfile_settings_.set_toolchain_label(Label());
 
+  dotfile_provider_ =
+      std::make_unique<ScopePerFileProvider>(&dotfile_scope_, false, true);
+
   build_settings_.set_item_defined_callback(
       [task_runner = scheduler_.task_runner(),
        builder = &builder_](std::unique_ptr<Item> item) {
diff --git a/src/gn/setup.h b/src/gn/setup.h
index b3abb1b..59ec134 100644
--- a/src/gn/setup.h
+++ b/src/gn/setup.h
@@ -15,6 +15,7 @@
 #include "gn/loader.h"
 #include "gn/scheduler.h"
 #include "gn/scope.h"
+#include "gn/scope_per_file_provider.h"
 #include "gn/settings.h"
 #include "gn/token.h"
 #include "gn/toolchain.h"
@@ -188,6 +189,7 @@
   // dot file.
   Settings dotfile_settings_;
   Scope dotfile_scope_;
+  std::unique_ptr<ScopePerFileProvider> dotfile_provider_;
 
   // State for invoking the dotfile.
   base::FilePath dotfile_name_;
diff --git a/src/gn/variables.cc b/src/gn/variables.cc
index 230427c..d99fccf 100644
--- a/src/gn/variables.cc
+++ b/src/gn/variables.cc
@@ -16,7 +16,10 @@
 const char kGnVersion_Help[] =
     R"(gn_version: [number] The version of gn.
 
-  Corresponds to the number printed by `gn --version`.
+  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).
 
 Example