Add presubmit check for bzl/bazel file formatting using `buildifier`

This only helps if you're using depot_tools.

Nothing else currently seems to check this upstream.
Incorrect formatting causes errors when importing into google3.

Change-Id: I439c207b7ca571ed25030cea639b89d10c990175
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/83827
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: Lily Chen <chlily@google.com>
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 4f13a4e..393d7f0 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -21,33 +21,102 @@
 USE_PYTHON3 = True
 
 
+def _PassResult(stdout, stderr, retcode):
+  return []  # Check passed.
+
+
+def _RunTool(input_api,
+             output_api,
+             command,
+             handle_result=_PassResult,
+             explain_error=None):
+  """
+  Runs a command and processes its result.
+  handle_result(stdout, stderr, retcode) and explain_error(error) should each
+  return a list of output_api.PresubmitResult.
+  """
+  try:
+    out, retcode = input_api.subprocess.communicate(
+        command,
+        stdout=input_api.subprocess.PIPE,
+        stderr=input_api.subprocess.PIPE,
+        encoding='utf-8')
+    return handle_result(out[0], out[1], retcode)
+  except input_api.subprocess.CalledProcessError as e:
+    errors = []
+    if explain_error:
+      errors += explain_error(e)
+    errors += [
+        output_api.PresubmitPromptOrNotify(
+            'Command "%s" returned exit code %d. Output: \n\n%s' %
+            ' '.join(command), e.returncode, e.output)
+    ]
+    return errors
+
+
 def CheckPregeneratedFiles(input_api, output_api):
   """Checks that pregenerated files are properly updated."""
   # TODO(chlily): Make this compatible with the util/bot environment for CI/CQ.
-  try:
-    # Check that `go` is available on the $PATH.
-    input_api.subprocess.check_call(['go', 'version'],
-                                    stdout=input_api.subprocess.PIPE,
-                                    stderr=input_api.subprocess.PIPE)
-  except input_api.subprocess.CalledProcessError as e:
-    return [
-        output_api.PresubmitPromptOrNotify(f'Could not run `go`: {e}')
-    ]
+  # Check that `go` is available on the $PATH.
+  if error := _RunTool(input_api, output_api, ['go', 'version']):
+    return error
 
-  pregenerate_script_path = input_api.os_path.join(
-      input_api.change.RepositoryRoot(), 'util', 'pregenerate')
-  try:
-    out, retcode = input_api.subprocess.communicate(
-        ['go', 'run', pregenerate_script_path, '-check'],
-        stdout=input_api.subprocess.PIPE,
-        stderr=input_api.subprocess.PIPE)
+  def HandlePregenerateResult(stdout, stderr, retcode):
     if retcode:
-      bad = out[1].decode("utf-8").splitlines()
       return [
           output_api.PresubmitError(
               ("Found out-of-date generated files. "
-               "Run `go run ./util/pregenerate` to update them."), bad)
+               "Run `go run ./util/pregenerate` to update them."),
+              stderr.splitlines())
       ]
-  except input_api.subprocess.CalledProcessError as e:
-    return [output_api.PresubmitError(f'Could not run go script: {e}')]
-  return []  # Check passed.
+    return []  # Check passed.
+
+  pregenerate_script_path = input_api.os_path.join(
+      input_api.change.RepositoryRoot(), 'util', 'pregenerate')
+  return _RunTool(input_api,
+                  output_api,
+                  ['go', 'run', pregenerate_script_path, '-check'],
+                  handle_result=HandlePregenerateResult)
+
+
+def CheckBuildifier(input_api, output_api):
+  """
+  Runs Buildifier formatting check if the affected files include
+  *.bazel or *.bzl files.
+  """
+  file_paths = []
+  for affected_file in input_api.AffectedFiles(include_deletes=False):
+    affected_file_path = affected_file.LocalPath()
+    if not affected_file_path.endswith(('.bzl', '.bazel')):
+      continue
+    if "third_party" in affected_file_path or "gen" in affected_file_path:
+      continue
+    file_paths.append(affected_file_path)
+  if not file_paths:
+    return []  # Check passed.
+
+  def ExplainBuildifierError(e):
+    return [
+        output_api.PresubmitNotifyResult(
+            ('You can download buildifier from '
+             'https://github.com/bazelbuild/buildtools/releases'))
+    ]
+
+  # Check that `buildifier` is available on the $PATH.
+  # TODO(chlily): Make this compatible with the util/bot environment for CI/CQ.
+  if error := _RunTool(input_api,
+                       output_api, ['buildifier', '--version'],
+                       explain_error=ExplainBuildifierError):
+    return error
+
+  def HandleBuildifierResult(stdout, stderr, retcode):
+    if retcode == 4:  # check mode failed (reformat is needed).
+      return [
+          output_api.PresubmitError(
+              ("Found incorrectly formatted *.bzl or *.bazel files. "
+               "Run `buildifier` to update them."), stdout.splitlines())
+      ]
+
+  return _RunTool(input_api,
+                  output_api, ['buildifier', '--mode=check'] + file_paths,
+                  handle_result=HandleBuildifierResult)