blob: cea0cafe7381f8a0dd1aa54babfe79fd1e1e202c [file] [log] [blame] [edit]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wrapper to run git-clang-format and parse its output."""
from __future__ import print_function
import hashlib
import io
import os
import sys
_path = os.path.realpath(__file__ + '/../../..')
if sys.path[0] != _path:
sys.path.insert(0, _path)
del _path
# The sys.path monkey patching confuses the linter.
# pylint: disable=wrong-import-position
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
# Since we're asking git-clang-format to print a diff, all modified filenames
# that have formatting errors are printed with this prefix.
DIFF_MARKER_PREFIX = '+++ b/'
BUILDTOOLS_PATH = os.path.join(constants.SOURCE_ROOT, 'src', 'chromium', 'src',
'buildtools')
def _GetSha1Hash(path):
"""Gets the SHA-1 hash of |path|, or None if the file does not exist."""
if not os.path.exists(path):
return None
with open(path, 'rb') as f:
m = hashlib.sha1()
while True:
buf = f.read(io.DEFAULT_BUFFER_SIZE)
if not buf:
break
m.update(buf)
return m.hexdigest()
def _GetDefaultClangFormatPath():
"""Gets the default clang-format binary path.
This also ensures that the binary itself is up-to-date.
"""
clang_format_path = os.path.join(BUILDTOOLS_PATH, 'linux64/clang-format')
hash_file_path = os.path.join(BUILDTOOLS_PATH, 'linux64/clang-format.sha1')
with open(hash_file_path, 'r') as f:
expected_hash = f.read().strip()
if expected_hash != _GetSha1Hash(clang_format_path):
# See chromium/src/buildtools/clang_format/README.txt for more details.
cmd = [os.path.join(constants.DEPOT_TOOLS_DIR,
'download_from_google_storage.py'), '-b',
'chromium-clang-format', '-s', hash_file_path]
cros_build_lib.run(cmd, print_cmd=False)
return clang_format_path
def main(argv):
"""Checks if a project is correctly formatted with clang-format.
Returns 1 if there are any clang-format-worthy changes in the project (or
on a provided list of files/directories in the project), 0 otherwise.
"""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument('--clang-format', default=_GetDefaultClangFormatPath(),
help='The path of the clang-format executable.')
parser.add_argument('--git-clang-format',
default=os.path.join(BUILDTOOLS_PATH, 'clang_format',
'script', 'git-clang-format'),
help='The path of the git-clang-format executable.')
parser.add_argument('--style', metavar='STYLE', type=str, default='file',
help='The style that clang-format will use.')
parser.add_argument('--extensions', metavar='EXTENSIONS', type=str,
help='Comma-separated list of file extensions to '
'format.')
parser.add_argument('--fix', action='store_true',
help='Fix any formatting errors automatically.')
scope = parser.add_mutually_exclusive_group(required=True)
scope.add_argument('--commit', type=str, default='HEAD',
help='Specify the commit to validate.')
scope.add_argument('--working-tree', action='store_true',
help='Validates the files that have changed from '
'HEAD in the working directory.')
parser.add_argument('files', type=str, nargs='*',
help='If specified, only consider differences in '
'these files/directories.')
opts = parser.parse_args(argv)
cmd = [opts.git_clang_format, '--binary', opts.clang_format, '--diff']
if opts.style:
cmd.extend(['--style', opts.style])
if opts.extensions:
cmd.extend(['--extensions', opts.extensions])
if not opts.working_tree:
cmd.extend(['%s^' % opts.commit, opts.commit])
cmd.extend(['--'] + opts.files)
# Fail gracefully if clang-format itself aborts/fails.
try:
result = cros_build_lib.run(cmd, print_cmd=False, stdout=True,
encoding='utf-8', errors='replace')
except cros_build_lib.RunCommandError as e:
print('clang-format failed:\n' + str(e), file=sys.stderr)
print('\nPlease report this to the clang team.', file=sys.stderr)
return 1
stdout = result.stdout
if stdout.rstrip('\n') == 'no modified files to format':
# This is always printed when only files that clang-format does not
# understand were modified.
return 0
diff_filenames = []
for line in stdout.splitlines():
if line.startswith(DIFF_MARKER_PREFIX):
diff_filenames.append(line[len(DIFF_MARKER_PREFIX):].rstrip())
if diff_filenames:
if opts.fix:
cros_build_lib.run(['git', 'apply'], print_cmd=False, input=stdout)
else:
print('The following files have formatting errors:')
for filename in diff_filenames:
print('\t%s' % filename)
print('You can run `%s --fix %s` to fix this' %
(sys.argv[0],
' '.join(cros_build_lib.ShellQuote(arg) for arg in argv)))
return 1
return 0
if __name__ == '__main__':
commandline.ScriptWrapperMain(lambda _: main)