1#!/usr/bin/env python
2# Copyright 2019 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5r"""Automatically fetch, build, and run clang-tidy from source.
6
7This script seeks to automate the steps detailed in docs/clang_tidy.md.
8
9Example: the following command disables clang-tidy's default checks (-*) and
10enables the clang static analyzer checks.
11
12    tools/clang/scripts/clang_tidy_tool.py \\
13       --checks='-*,clang-analyzer-*,-clang-analyzer-alpha*' \\
14       --header-filter='.*' \\
15       out/Release chrome
16
17The same, but checks the changes only.
18
19    git diff -U5 | tools/clang/scripts/clang_tidy_tool.py \\
20       --diff \\
21       --checks='-*,clang-analyzer-*,-clang-analyzer-alpha*' \\
22       --header-filter='.*' \\
23       out/Release chrome
24"""
25
26from __future__ import print_function
27
28import argparse
29import os
30import subprocess
31import sys
32import update
33
34import build_clang_tools_extra
35
36
37def GetBinaryPath(build_dir, binary):
38  if sys.platform == 'win32':
39    binary += '.exe'
40  return os.path.join(build_dir, 'bin', binary)
41
42
43def BuildNinjaTarget(out_dir, ninja_target):
44  args = ['autoninja', '-C', out_dir, ninja_target]
45  subprocess.check_call(args, shell=sys.platform == 'win32')
46
47
48def GenerateCompDb(out_dir):
49  gen_compdb_script = os.path.join(
50      os.path.dirname(__file__), 'generate_compdb.py')
51  comp_db_file_path = os.path.join(out_dir, 'compile_commands.json')
52  args = [
53      sys.executable,
54      gen_compdb_script,
55      '-p',
56      out_dir,
57      '-o',
58      comp_db_file_path,
59  ]
60  subprocess.check_call(args)
61
62  # The resulting CompDb file includes /showIncludes which causes clang-tidy to
63  # output a lot of unnecessary text to the console.
64  with open(comp_db_file_path, 'r') as comp_db_file:
65    comp_db_data = comp_db_file.read();
66
67  # The trailing space on /showIncludes helps keep single-spaced flags.
68  comp_db_data = comp_db_data.replace('/showIncludes ', '')
69
70  with open(comp_db_file_path, 'w') as comp_db_file:
71    comp_db_file.write(comp_db_data)
72
73
74def RunClangTidy(checks, header_filter, auto_fix, clang_src_dir,
75                 clang_build_dir, out_dir, ninja_target):
76  """Invoke the |run-clang-tidy.py| script."""
77  run_clang_tidy_script = os.path.join(
78      clang_src_dir, 'clang-tools-extra', 'clang-tidy', 'tool',
79      'run-clang-tidy.py')
80
81  clang_tidy_binary = GetBinaryPath(clang_build_dir, 'clang-tidy')
82  clang_apply_rep_binary = GetBinaryPath(clang_build_dir,
83                                         'clang-apply-replacements')
84
85  args = [
86      sys.executable,
87      run_clang_tidy_script,
88      '-quiet',
89      '-p',
90      out_dir,
91      '-clang-tidy-binary',
92      clang_tidy_binary,
93      '-clang-apply-replacements-binary',
94      clang_apply_rep_binary,
95  ]
96
97  if checks:
98    args.append('-checks={}'.format(checks))
99
100  if header_filter:
101    args.append('-header-filter={}'.format(header_filter))
102
103  if auto_fix:
104    args.append('-fix')
105
106  args.append(ninja_target)
107  subprocess.check_call(args)
108
109
110def RunClangTidyDiff(checks, auto_fix, clang_src_dir, clang_build_dir, out_dir):
111  """Invoke the |clang-tidy-diff.py| script over the diff from stdin."""
112  clang_tidy_diff_script = os.path.join(
113      clang_src_dir, 'clang-tools-extra', 'clang-tidy', 'tool',
114      'clang-tidy-diff.py')
115
116  clang_tidy_binary = GetBinaryPath(clang_build_dir, 'clang-tidy')
117
118  args = [
119      clang_tidy_diff_script,
120      '-quiet',
121      '-p1',
122      '-path',
123      out_dir,
124      '-clang-tidy-binary',
125      clang_tidy_binary,
126  ]
127
128  if checks:
129    args.append('-checks={}'.format(checks))
130
131  if auto_fix:
132    args.append('-fix')
133
134  subprocess.check_call(args)
135
136
137def main():
138  script_name = sys.argv[0]
139
140  parser = argparse.ArgumentParser(
141      formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
142  parser.add_argument(
143      '--fetch',
144      nargs='?',
145      const=update.CLANG_REVISION,
146      help='Fetch and build clang sources')
147  parser.add_argument(
148      '--build',
149      action='store_true',
150      help='build clang sources to get clang-tidy')
151  parser.add_argument(
152      '--diff',
153      action='store_true',
154      default=False,
155      help ='read diff from the stdin and check it')
156  parser.add_argument('--clang-src-dir', type=str,
157                      help='override llvm and clang checkout location')
158  parser.add_argument('--clang-build-dir', type=str,
159                      help='override clang build dir location')
160  parser.add_argument('--checks', help='passed to clang-tidy')
161  parser.add_argument('--header-filter', help='passed to clang-tidy')
162  parser.add_argument(
163      '--auto-fix',
164      action='store_true',
165      help='tell clang-tidy to auto-fix errors')
166  parser.add_argument('OUT_DIR', help='where we are building Chrome')
167  parser.add_argument('NINJA_TARGET', help='ninja target')
168  args = parser.parse_args()
169
170  steps = []
171
172  # If the user hasn't provided a clang checkout and build dir, checkout and
173  # build clang-tidy where update.py would.
174  if not args.clang_src_dir:
175    args.clang_src_dir = build_clang_tools_extra.GetCheckoutDir(args.OUT_DIR)
176  if not args.clang_build_dir:
177    args.clang_build_dir = build_clang_tools_extra.GetBuildDir(args.OUT_DIR)
178  elif (args.clang_build_dir and not
179        os.path.isfile(GetBinaryPath(args.clang_build_dir, 'clang-tidy'))):
180    sys.exit('clang-tidy binary doesn\'t exist at ' +
181             GetBinaryPath(args.clang_build_dir, 'clang-tidy'))
182
183  if args.fetch:
184    steps.append(('Fetching LLVM sources', lambda:
185                  build_clang_tools_extra.FetchLLVM(args.clang_src_dir,
186                                                    args.fetch)))
187
188  if args.build:
189    steps.append(('Building clang-tidy',
190                  lambda: build_clang_tools_extra.BuildTargets(
191                      args.clang_build_dir,
192                      ['clang-tidy', 'clang-apply-replacements'])))
193
194  steps += [
195      ('Building ninja target: %s' % args.NINJA_TARGET,
196      lambda: BuildNinjaTarget(args.OUT_DIR, args.NINJA_TARGET)),
197      ('Generating compilation DB', lambda: GenerateCompDb(args.OUT_DIR))
198    ]
199  if args.diff:
200    steps += [
201        ('Running clang-tidy on diff', lambda: RunClangTidyDiff(
202            args.checks, args.auto_fix, args.clang_src_dir, args.
203            clang_build_dir, args.OUT_DIR)),
204    ]
205  else:
206    steps += [
207        ('Running clang-tidy',
208        lambda: RunClangTidy(args.checks, args.header_filter,
209                             args.auto_fix, args.clang_src_dir,
210                             args.clang_build_dir, args.OUT_DIR,
211                             args.NINJA_TARGET)),
212    ]
213
214  # Run the steps in sequence.
215  for i, (msg, step_func) in enumerate(steps):
216    # Print progress message
217    print('-- %s %s' % (script_name, '-' * (80 - len(script_name) - 4)))
218    print('-- [%d/%d] %s' % (i + 1, len(steps), msg))
219    print(80 * '-')
220
221    step_func()
222
223  return 0
224
225
226if __name__ == '__main__':
227  sys.exit(main())
228