Support specifying required Ninja version

Newer Ninja versions support features that may allow for faster, more
correct builds, but some GN users like Chromium are stuck at older
Ninja versions so we cannot use these features unconditionally.

This change introduces new .gn variable: ninja_required_version. This
variable is directly propagated to the generated Ninja file, and can
be also used inside GN to gate the use of certain features.

Change-Id: I95c0f6af2b62a6581a6055232eaf3e941803fd9b
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/8721
Commit-Queue: Petr Hosek <phosek@google.com>
Reviewed-by: Brett Wilson <brettw@chromium.org>
diff --git a/build/gen.py b/build/gen.py
index a00558b..99e06f7 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -598,6 +598,7 @@
         'src/gn/value.cc',
         'src/gn/value_extractors.cc',
         'src/gn/variables.cc',
+        'src/gn/version.cc',
         'src/gn/visibility.cc',
         'src/gn/visual_studio_utils.cc',
         'src/gn/visual_studio_writer.cc',
@@ -694,6 +695,7 @@
         'src/gn/unique_vector_unittest.cc',
         'src/gn/value_unittest.cc',
         'src/gn/vector_utils_unittest.cc',
+        'src/gn/version_unittest.cc',
         'src/gn/visibility_unittest.cc',
         'src/gn/visual_studio_utils_unittest.cc',
         'src/gn/visual_studio_writer_unittest.cc',
diff --git a/docs/reference.md b/docs/reference.md
index 0dac783..79b01a2 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -6662,6 +6662,11 @@
       GN will look for build files named "BUILD.$build_file_extension.gn".
       This is intended to be used during migrations or other situations where
       there are two independent GN builds in the same directories.
+
+  ninja_required_version [optional]
+      When set specifies the minimum required version of Ninja. The default
+      required version is 1.7.2. Specifying a higher version might enable the
+      use of some of newer features that can make the build more efficient.
 ```
 
 #### **Example .gn file contents**
diff --git a/src/gn/build_settings.cc b/src/gn/build_settings.cc
index 9935239..22c1145 100644
--- a/src/gn/build_settings.cc
+++ b/src/gn/build_settings.cc
@@ -17,6 +17,7 @@
       root_path_utf8_(other.root_path_utf8_),
       secondary_source_path_(other.secondary_source_path_),
       python_path_(other.python_path_),
+      ninja_required_version_(other.ninja_required_version_),
       build_config_file_(other.build_config_file_),
       arg_file_template_path_(other.arg_file_template_path_),
       build_dir_(other.build_dir_),
diff --git a/src/gn/build_settings.h b/src/gn/build_settings.h
index 0bb55e9..f8dc502 100644
--- a/src/gn/build_settings.h
+++ b/src/gn/build_settings.h
@@ -18,6 +18,7 @@
 #include "gn/scope.h"
 #include "gn/source_dir.h"
 #include "gn/source_file.h"
+#include "gn/version.h"
 
 class Item;
 
@@ -55,6 +56,12 @@
   base::FilePath python_path() const { return python_path_; }
   void set_python_path(const base::FilePath& p) { python_path_ = p; }
 
+  // Required Ninja version.
+  const Version& ninja_required_version() const {
+    return ninja_required_version_;
+  }
+  void set_ninja_required_version(Version v) { ninja_required_version_ = v; }
+
   const SourceFile& build_config_file() const { return build_config_file_; }
   void set_build_config_file(const SourceFile& f) { build_config_file_ = f; }
 
@@ -126,6 +133,9 @@
   base::FilePath secondary_source_path_;
   base::FilePath python_path_;
 
+  // See 40045b9 for the reason behind using 1.7.2 as the default version.
+  Version ninja_required_version_{1, 7, 2};
+
   SourceFile build_config_file_;
   SourceFile arg_file_template_path_;
   SourceDir build_dir_;
diff --git a/src/gn/ninja_build_writer.cc b/src/gn/ninja_build_writer.cc
index b5b491b..f801fb6 100644
--- a/src/gn/ninja_build_writer.cc
+++ b/src/gn/ninja_build_writer.cc
@@ -277,7 +277,8 @@
 }
 
 void NinjaBuildWriter::WriteNinjaRules() {
-  out_ << "ninja_required_version = 1.7.2\n\n";
+  out_ << "ninja_required_version = "
+       << build_settings_->ninja_required_version().Describe() << "\n\n";
   out_ << "rule gn\n";
   out_ << "  command = " << GetSelfInvocationCommand(build_settings_) << "\n";
   out_ << "  description = Regenerating ninja files\n\n";
diff --git a/src/gn/setup.cc b/src/gn/setup.cc
index 4026d1a..1d2bea2 100644
--- a/src/gn/setup.cc
+++ b/src/gn/setup.cc
@@ -146,6 +146,11 @@
       This is intended to be used during migrations or other situations where
       there are two independent GN builds in the same directories.
 
+  ninja_required_version [optional]
+      When set specifies the minimum required version of Ninja. The default
+      required version is 1.7.2. Specifying a higher version might enable the
+      use of some of newer features that can make the build more efficient.
+
 Example .gn file contents
 
   buildconfig = "//build/config/BUILDCONFIG.gn"
@@ -809,6 +814,25 @@
     loader_->set_build_file_extension(extension);
   }
 
+  // Ninja required version.
+  const Value* ninja_required_version_value =
+      dotfile_scope_.GetValue("ninja_required_version", true);
+  if (ninja_required_version_value) {
+    if (!ninja_required_version_value->VerifyTypeIs(Value::STRING, &err)) {
+      err.PrintToStdout();
+      return false;
+    }
+    std::optional<Version> version =
+        Version::FromString(ninja_required_version_value->string_value());
+    if (!version) {
+      Err(Location(), "Invalid Ninja version '" +
+                          ninja_required_version_value->string_value() + "'")
+          .PrintToStdout();
+      return false;
+    }
+    build_settings_.set_ninja_required_version(*version);
+  }
+
   // Root build file.
   const Value* root_value = dotfile_scope_.GetValue("root", true);
   if (root_value) {
diff --git a/src/gn/version.cc b/src/gn/version.cc
new file mode 100644
index 0000000..d5fea31
--- /dev/null
+++ b/src/gn/version.cc
@@ -0,0 +1,79 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "gn/version.h"
+#include <iostream>
+#include <string_view>
+
+#include "base/strings/string_number_conversions.h"
+
+using namespace std::literals;
+
+constexpr std::string_view kDot = "."sv;
+
+Version::Version(int major, int minor, int patch)
+    : major_(major), minor_(minor), patch_(patch) {}
+
+// static
+std::optional<Version> Version::FromString(std::string s) {
+  int major = 0, minor = 0, patch = 0;
+  // First, parse the major version.
+  size_t major_begin = 0;
+  if (size_t major_end = s.find(kDot, major_begin);
+      major_end != std::string::npos) {
+    if (!base::StringToInt(s.substr(major_begin, major_end - major_begin),
+                           &major))
+      return {};
+    // Then, parse the minor version.
+    size_t minor_begin = major_end + kDot.size();
+    if (size_t minor_end = s.find(kDot, minor_begin);
+        minor_end != std::string::npos) {
+      if (!base::StringToInt(s.substr(minor_begin, minor_end - minor_begin),
+                             &minor))
+        return {};
+      // Finally, parse the patch version.
+      size_t patch_begin = minor_end + kDot.size();
+      if (!base::StringToInt(s.substr(patch_begin, std::string::npos), &patch))
+        return {};
+      return Version(major, minor, patch);
+    }
+  }
+  return {};
+}
+
+bool Version::operator==(const Version& other) const {
+  return other.major_ == major_ && other.minor_ == minor_ &&
+         other.patch_ == patch_;
+}
+
+bool Version::operator<(const Version& other) const {
+  return std::tie(major_, minor_, patch_) <
+         std::tie(other.major_, other.minor_, other.patch_);
+}
+
+bool Version::operator!=(const Version& other) const {
+  return !(*this == other);
+}
+
+bool Version::operator>=(const Version& other) const {
+  return !(*this < other);
+}
+
+bool Version::operator>(const Version& other) const {
+  return other < *this;
+}
+
+bool Version::operator<=(const Version& other) const {
+  return !(*this > other);
+}
+
+std::string Version::Describe() const {
+  std::string ret;
+  ret += base::IntToString(major_);
+  ret += kDot;
+  ret += base::IntToString(minor_);
+  ret += kDot;
+  ret += base::IntToString(patch_);
+  return ret;
+}
diff --git a/src/gn/version.h b/src/gn/version.h
new file mode 100644
index 0000000..7fcf81d
--- /dev/null
+++ b/src/gn/version.h
@@ -0,0 +1,37 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TOOLS_GN_VERSION_H_
+#define TOOLS_GN_VERSION_H_
+
+#include <optional>
+#include <string>
+
+// Represents a semantic version.
+class Version {
+ public:
+  Version(int major, int minor, int patch);
+
+  static std::optional<Version> FromString(std::string s);
+
+  int major() const { return major_; }
+  int minor() const { return minor_; }
+  int patch() const { return patch_; }
+
+  bool operator==(const Version& other) const;
+  bool operator<(const Version& other) const;
+  bool operator!=(const Version& other) const;
+  bool operator>=(const Version& other) const;
+  bool operator>(const Version& other) const;
+  bool operator<=(const Version& other) const;
+
+  std::string Describe() const;
+
+ private:
+  int major_;
+  int minor_;
+  int patch_;
+};
+
+#endif  // TOOLS_GN_VERSION_H_
diff --git a/src/gn/version_unittest.cc b/src/gn/version_unittest.cc
new file mode 100644
index 0000000..d1530f3
--- /dev/null
+++ b/src/gn/version_unittest.cc
@@ -0,0 +1,33 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "gn/version.h"
+
+#include "util/test/test.h"
+
+TEST(VersionTest, FromString) {
+  Version v0_0_1{0, 0, 1};
+  ASSERT_EQ(Version::FromString("0.0.1"), v0_0_1);
+  Version v0_1_0{0, 1, 0};
+  ASSERT_EQ(Version::FromString("0.1.0"), v0_1_0);
+  Version v1_0_0{1, 0, 0};
+  ASSERT_EQ(Version::FromString("1.0.0"), v1_0_0);
+}
+
+TEST(VersionTest, Comparison) {
+  Version v0_0_1{0, 0, 1};
+  Version v0_1_0{0, 1, 0};
+  ASSERT_TRUE(v0_0_1 == v0_0_1);
+  ASSERT_TRUE(v0_0_1 != v0_1_0);
+  ASSERT_TRUE(v0_0_1 <= v0_0_1);
+  ASSERT_TRUE(v0_0_1 <= v0_1_0);
+  ASSERT_TRUE(v0_0_1 < v0_1_0);
+  ASSERT_TRUE(v0_0_1 >= v0_0_1);
+  ASSERT_TRUE(v0_1_0 > v0_0_1);
+  ASSERT_TRUE(v0_1_0 >= v0_0_1);
+}
+
+TEST(VersionTest, Describe) {
+  ASSERT_EQ(Version::FromString("0.0.1")->Describe(), "0.0.1");
+}