#!/usr/bin/env python import subprocess import yaml import json from multiprocessing import Pool, cpu_count import shutil import os import re import argparse def call(args): '''Subprocess run wrapper.''' completed = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) error_msg = completed.stdout.decode('utf-8') matched = re.match('.*xgboost.*warning.*', error_msg, re.MULTILINE | re.DOTALL) if matched is None: return_code = 0 else: print(error_msg, '\n') return_code = 1 return completed.returncode | return_code class ClangTidy(object): ''' clang tidy wrapper. Args: gtest_path: Full path of Google Test library. cpp_lint: Run linter on C++ source code. cuda_lint: Run linter on CUDA source code. ''' def __init__(self, gtest_path, cpp_lint, cuda_lint): self.gtest_path = gtest_path self.cpp_lint = cpp_lint self.cuda_lint = cuda_lint print('Using Google Test from {}'.format(self.gtest_path)) print('Run linter on CUDA: ', self.cuda_lint) print('Run linter on C++:', self.cpp_lint) if not self.cpp_lint and not self.cuda_lint: raise ValueError('Both --cpp and --cuda are set to 0.') self.root_path = os.path.abspath(os.path.curdir) print('Project root:', self.root_path) self.cdb_path = os.path.join(self.root_path, 'cdb') def __enter__(self): if os.path.exists(self.cdb_path): shutil.rmtree(self.cdb_path) self._generate_cdb() return self def __exit__(self, *args): if os.path.exists(self.cdb_path): shutil.rmtree(self.cdb_path) def _generate_cdb(self): '''Run CMake to generate compilation database.''' os.mkdir(self.cdb_path) os.chdir(self.cdb_path) cmake_args = ['cmake', '..', '-DGENERATE_COMPILATION_DATABASE=ON', '-DGOOGLE_TEST=ON', '-DGTEST_ROOT={}'.format( self.gtest_path)] if self.cuda_lint: cmake_args.extend(['-DUSE_CUDA=ON', '-DUSE_NCCL=ON']) subprocess.run(cmake_args) os.chdir(self.root_path) def _configure_flags(self, path, command): common_args = ['clang-tidy', # "-header-filter='(xgboost\\/src|xgboost\\/include)'", '-config='+str(self.clang_tidy)] common_args.append(path) common_args.append('--') command = command.split()[1:] # remove clang/c++/g++ # filter out not used flags if '-fuse-ld=gold' in command: command.remove('-fuse-ld=gold') if '-rdynamic' in command: command.remove('-rdynamic') if '-Xcompiler=-fPIC' in command: command.remove('-Xcompiler=-fPIC') if '-Xcompiler=-fPIE' in command: command.remove('-Xcompiler=-fPIE') if '-c' in command: index = command.index('-c') del command[index+1] command.remove('-c') if '-o' in command: index = command.index('-o') del command[index+1] command.remove('-o') common_args.extend(command) # Two passes, one for device code another for host code. if path.endswith('cu'): args = [common_args.copy(), common_args.copy()] args[0].append('--cuda-host-only') args[1].append('--cuda-device-only') else: args = [common_args.copy()] for a in args: a.append('-Wno-unused-command-line-argument') return args def _configure(self): '''Load and configure compile_commands and clang_tidy.''' def should_lint(path): if not self.cpp_lint and path.endswith('.cc'): return False return True cdb_file = os.path.join(self.cdb_path, 'compile_commands.json') with open(cdb_file, 'r') as fd: self.compile_commands = json.load(fd) tidy_file = os.path.join(self.root_path, '.clang-tidy') with open(tidy_file) as fd: self.clang_tidy = yaml.load(fd) all_files = [] for entry in self.compile_commands: path = entry['file'] if should_lint(path): print(path) args = self._configure_flags(path, entry['command']) all_files.extend(args) return all_files def run(self): '''Run clang-tidy.''' all_files = self._configure() with Pool(cpu_count()) as pool: results = pool.map(call, all_files) passed = True if 1 in results: print('Please correct clang-tidy warnings.') passed = False return passed if __name__ == '__main__': parser = argparse.ArgumentParser(description='Run clang-tidy.') parser.add_argument('--cpp', type=int, default=1) parser.add_argument('--cuda', type=int, default=1) parser.add_argument('--gtest-path', required=True, help='Full path of Google Test library directory') args = parser.parse_args() with ClangTidy(args.gtest_path, args.cpp, args.cuda) as linter: passed = linter.run() # Uncomment it once the code base is clang-tidy conformant. # if not passed: # sys.exit(1)