Add gn check presubmit

This runs `gn gen --check` with default args to detect mismatches between
#includes and dependencies in the BUILD.gn files, as well as general build
errors. Run this before uploading a CL for early detection, otherwise such
errors will cause per-platform try jobs to fail.

Bug: webrtc:8279
Change-Id: Ib87e2e3f40b8d1146ea5c1202fb113508a3f05e3
Reviewed-on: https://webrtc-review.googlesource.com/5482
Commit-Queue: Oleh Prypin <oprypin@webrtc.org>
Reviewed-by: Henrik Kjellander <kjellander@webrtc.org>
Reviewed-by: Patrik Höglund <phoglund@webrtc.org>
Reviewed-by: Edward Lemur <ehmaldonado@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#20208}
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index ff46605..a356d76 100755
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -406,6 +406,22 @@
                                                    output_api))
   return result
 
+def CheckGnGen(input_api, output_api):
+  """Runs `gn gen --check` with default args to detect mismatches between
+  #includes and dependencies in the BUILD.gn files, as well as general build
+  errors.
+  """
+  with _AddToPath(input_api.os_path.join(
+      input_api.PresubmitLocalPath(), 'tools_webrtc', 'presubmit_checks_lib')):
+    from gn_check import RunGnCheck
+  errors = RunGnCheck(input_api.PresubmitLocalPath())[:5]
+  if errors:
+    return [output_api.PresubmitPromptWarning(
+        'Some #includes do not match the build dependency graph. Please run:\n'
+        '  gn gen --check <out_dir>',
+        long_text='\n\n'.join(errors))]
+  return []
+
 def CheckUnwantedDependencies(input_api, output_api):
   """Runs checkdeps on #include statements added in this
   change. Breaking - rules is an error, breaking ! rules is a
@@ -673,6 +689,7 @@
 def CheckChangeOnUpload(input_api, output_api):
   results = []
   results.extend(CommonChecks(input_api, output_api))
+  results.extend(CheckGnGen(input_api, output_api))
   results.extend(
       input_api.canned_checks.CheckGNFormatted(input_api, output_api))
   return results
diff --git a/tools_webrtc/presubmit_checks_lib/gn_check.py b/tools_webrtc/presubmit_checks_lib/gn_check.py
new file mode 100644
index 0000000..7270bdd
--- /dev/null
+++ b/tools_webrtc/presubmit_checks_lib/gn_check.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS.  All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+import re
+import shutil
+import subprocess
+import tempfile
+
+
+# GN_ERROR_RE matches the summary of an error output by `gn check`.
+# Matches "ERROR" and following lines until it sees an empty line or a line
+# containing just underscores.
+GN_ERROR_RE = re.compile(r'^ERROR .+(?:\n.*[^_\n].*$)+', re.MULTILINE)
+
+
+def RunGnCheck(root_dir=None):
+  """Runs `gn gen --check` with default args to detect mismatches between
+  #includes and dependencies in the BUILD.gn files, as well as general build
+  errors.
+
+  Returns a list of error summary strings.
+  """
+  out_dir = tempfile.mkdtemp('gn')
+  try:
+    command = ['gn', 'gen', '--check', out_dir]
+    subprocess.check_output(command, cwd=root_dir)
+  except subprocess.CalledProcessError as err:
+    return GN_ERROR_RE.findall(err.output)
+  else:
+    return []
+  finally:
+    shutil.rmtree(out_dir, ignore_errors=True)
diff --git a/tools_webrtc/presubmit_checks_lib/gn_check_test.py b/tools_webrtc/presubmit_checks_lib/gn_check_test.py
new file mode 100755
index 0000000..f7e158c
--- /dev/null
+++ b/tools_webrtc/presubmit_checks_lib/gn_check_test.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS.  All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+import os
+import unittest
+
+from gn_check import RunGnCheck
+
+
+TESTDATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                            'testdata')
+
+
+class GnCheckTest(unittest.TestCase):
+  def testCircularDependencyError(self):
+    test_dir = os.path.join(TESTDATA_DIR, 'circular_dependency')
+    expected_errors = ['ERROR Dependency cycle:\n'
+                       '  //:bar ->\n  //:foo ->\n  //:bar']
+    self.assertListEqual(expected_errors, RunGnCheck(test_dir))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/.gn b/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/.gn
new file mode 100644
index 0000000..9fe0b42
--- /dev/null
+++ b/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/.gn
@@ -0,0 +1 @@
+buildconfig = "//BUILDCONFIG.gn"
diff --git a/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/BUILD.gn b/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/BUILD.gn
new file mode 100644
index 0000000..cf17887
--- /dev/null
+++ b/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/BUILD.gn
@@ -0,0 +1,14 @@
+toolchain("toolchain") {
+}
+
+static_library("foo") {
+  deps = [
+    ":bar",
+  ]
+}
+
+static_library("bar") {
+  deps = [
+    ":foo",
+  ]
+}
diff --git a/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/BUILDCONFIG.gn b/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/BUILDCONFIG.gn
new file mode 100644
index 0000000..48c2a46
--- /dev/null
+++ b/tools_webrtc/presubmit_checks_lib/testdata/circular_dependency/BUILDCONFIG.gn
@@ -0,0 +1 @@
+set_default_toolchain(":toolchain")