run-clang-tidy.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import * as minimist from 'minimist';
  2. import * as streamChain from 'stream-chain';
  3. import * as streamJson from 'stream-json';
  4. import { ignore as streamJsonIgnore } from 'stream-json/filters/Ignore';
  5. import { streamArray as streamJsonStreamArray } from 'stream-json/streamers/StreamArray';
  6. import * as childProcess from 'node:child_process';
  7. import * as fs from 'node:fs';
  8. import * as os from 'node:os';
  9. import * as path from 'node:path';
  10. import { chunkFilenames, findMatchingFiles } from './lib/utils';
  11. const SOURCE_ROOT = path.normalize(path.dirname(__dirname));
  12. const LLVM_BIN = path.resolve(
  13. SOURCE_ROOT,
  14. '..',
  15. 'third_party',
  16. 'llvm-build',
  17. 'Release+Asserts',
  18. 'bin'
  19. );
  20. const PLATFORM = os.platform();
  21. type SpawnAsyncResult = {
  22. stdout: string;
  23. stderr: string;
  24. status: number | null;
  25. };
  26. class ErrorWithExitCode extends Error {
  27. exitCode: number;
  28. constructor (message: string, exitCode: number) {
  29. super(message);
  30. this.exitCode = exitCode;
  31. }
  32. }
  33. async function spawnAsync (
  34. command: string,
  35. args: string[],
  36. options?: childProcess.SpawnOptionsWithoutStdio | undefined
  37. ): Promise<SpawnAsyncResult> {
  38. return new Promise((resolve, reject) => {
  39. try {
  40. const stdio = { stdout: '', stderr: '' };
  41. const spawned = childProcess.spawn(command, args, options || {});
  42. spawned.stdout.on('data', (data) => {
  43. stdio.stdout += data;
  44. });
  45. spawned.stderr.on('data', (data) => {
  46. stdio.stderr += data;
  47. });
  48. spawned.on('exit', (code) => resolve({ ...stdio, status: code }));
  49. spawned.on('error', (err) => reject(err));
  50. } catch (err) {
  51. reject(err);
  52. }
  53. });
  54. }
  55. function getDepotToolsEnv (): NodeJS.ProcessEnv {
  56. let depotToolsEnv;
  57. const findDepotToolsOnPath = () => {
  58. const result = childProcess.spawnSync(
  59. PLATFORM === 'win32' ? 'where' : 'which',
  60. ['gclient']
  61. );
  62. if (result.status === 0) {
  63. return process.env;
  64. }
  65. };
  66. const checkForBuildTools = () => {
  67. const result = childProcess.spawnSync(
  68. 'electron-build-tools',
  69. ['show', 'env', '--json'],
  70. { shell: true }
  71. );
  72. if (result.status === 0) {
  73. return {
  74. ...process.env,
  75. ...JSON.parse(result.stdout.toString().trim())
  76. };
  77. }
  78. };
  79. try {
  80. depotToolsEnv = findDepotToolsOnPath();
  81. if (!depotToolsEnv) depotToolsEnv = checkForBuildTools();
  82. } catch {}
  83. if (!depotToolsEnv) {
  84. throw new Error("Couldn't find depot_tools, ensure it's on your PATH");
  85. }
  86. if (!('CHROMIUM_BUILDTOOLS_PATH' in depotToolsEnv)) {
  87. throw new Error(
  88. 'CHROMIUM_BUILDTOOLS_PATH environment variable must be set'
  89. );
  90. }
  91. return depotToolsEnv;
  92. }
  93. async function runClangTidy (
  94. outDir: string,
  95. filenames: string[],
  96. checks: string = '',
  97. jobs: number = 1
  98. ): Promise<boolean> {
  99. const cmd = path.resolve(LLVM_BIN, 'clang-tidy');
  100. const args = [`-p=${outDir}`, '--use-color'];
  101. if (checks) args.push(`--checks=${checks}`);
  102. // Remove any files that aren't in the compilation database to prevent
  103. // errors from cluttering up the output. Since the compilation DB is hundreds
  104. // of megabytes, this is done with streaming to not hold it all in memory.
  105. const filterCompilationDatabase = (): Promise<string[]> => {
  106. const compiledFilenames: string[] = [];
  107. return new Promise((resolve) => {
  108. const pipeline = streamChain.chain([
  109. fs.createReadStream(path.resolve(outDir, 'compile_commands.json')),
  110. streamJson.parser(),
  111. streamJsonIgnore({ filter: /\bcommand\b/i }),
  112. streamJsonStreamArray(),
  113. ({ value: { file, directory } }) => {
  114. const filename = path.resolve(directory, file);
  115. return filenames.includes(filename) ? filename : null;
  116. }
  117. ]);
  118. pipeline.on('data', (data) => compiledFilenames.push(data));
  119. pipeline.on('end', () => resolve(compiledFilenames));
  120. });
  121. };
  122. // clang-tidy can figure out the file from a short relative filename, so
  123. // to get the most bang for the buck on the command line, let's trim the
  124. // filenames to the minimum so that we can fit more per invocation
  125. filenames = (await filterCompilationDatabase()).map((filename) =>
  126. path.relative(SOURCE_ROOT, filename)
  127. );
  128. if (filenames.length === 0) {
  129. throw new Error('No filenames to run');
  130. }
  131. const commandLength =
  132. cmd.length + args.reduce((length, arg) => length + arg.length, 0);
  133. const results: boolean[] = [];
  134. const asyncWorkers = [];
  135. const chunkedFilenames: string[][] = [];
  136. const filesPerWorker = Math.ceil(filenames.length / jobs);
  137. for (let i = 0; i < jobs; i++) {
  138. chunkedFilenames.push(
  139. ...chunkFilenames(filenames.splice(0, filesPerWorker), commandLength)
  140. );
  141. }
  142. const worker = async () => {
  143. let filenames = chunkedFilenames.shift();
  144. while (filenames?.length) {
  145. results.push(
  146. await spawnAsync(cmd, [...args, ...filenames], {}).then((result) => {
  147. console.log(result.stdout);
  148. if (result.status !== 0) {
  149. console.error(result.stderr);
  150. }
  151. // On a clean run there's nothing on stdout. A run with warnings-only
  152. // will have a status code of zero, but there's output on stdout
  153. return result.status === 0 && result.stdout.length === 0;
  154. })
  155. );
  156. filenames = chunkedFilenames.shift();
  157. }
  158. };
  159. for (let i = 0; i < jobs; i++) {
  160. asyncWorkers.push(worker());
  161. }
  162. try {
  163. await Promise.all(asyncWorkers);
  164. return results.every((x) => x);
  165. } catch {
  166. return false;
  167. }
  168. }
  169. function parseCommandLine () {
  170. const showUsage = (arg?: string) : boolean => {
  171. if (!arg || arg.startsWith('-')) {
  172. console.log(
  173. 'Usage: script/run-clang-tidy.ts [-h|--help] [--jobs|-j] ' +
  174. '[--checks] --out-dir OUTDIR [file1 file2]'
  175. );
  176. process.exit(0);
  177. }
  178. return true;
  179. };
  180. const opts = minimist(process.argv.slice(2), {
  181. boolean: ['help'],
  182. string: ['checks', 'out-dir'],
  183. default: { jobs: 1 },
  184. alias: { help: 'h', jobs: 'j' },
  185. stopEarly: true,
  186. unknown: showUsage
  187. });
  188. if (opts.help) showUsage();
  189. if (!opts['out-dir']) {
  190. console.log('--out-dir is a required argument');
  191. process.exit(0);
  192. }
  193. return opts;
  194. }
  195. async function main (): Promise<boolean> {
  196. const opts = parseCommandLine();
  197. const outDir = path.resolve(opts['out-dir']);
  198. if (!fs.existsSync(outDir)) {
  199. throw new Error("Output directory doesn't exist");
  200. } else {
  201. // Make sure the compile_commands.json file is up-to-date
  202. const env = getDepotToolsEnv();
  203. const result = childProcess.spawnSync(
  204. 'gn',
  205. ['gen', '.', '--export-compile-commands'],
  206. { cwd: outDir, env, shell: true }
  207. );
  208. if (result.status !== 0) {
  209. if (result.error) {
  210. console.error(result.error.message);
  211. } else {
  212. console.error(result.stderr.toString());
  213. }
  214. throw new ErrorWithExitCode(
  215. 'Failed to automatically generate compile_commands.json for ' +
  216. 'output directory',
  217. 2
  218. );
  219. }
  220. }
  221. const filenames = [];
  222. if (opts._.length > 0) {
  223. filenames.push(...opts._.map((filename) => path.resolve(filename)));
  224. } else {
  225. filenames.push(
  226. ...(await findMatchingFiles(
  227. path.resolve(SOURCE_ROOT, 'shell'),
  228. (filename: string) => /.*\.(?:cc|h|mm)$/.test(filename)
  229. ))
  230. );
  231. }
  232. return runClangTidy(outDir, filenames, opts.checks, opts.jobs);
  233. }
  234. if (require.main === module) {
  235. main()
  236. .then((success) => {
  237. process.exit(success ? 0 : 1);
  238. })
  239. .catch((err: ErrorWithExitCode) => {
  240. console.error(`ERROR: ${err.message}`);
  241. process.exit(err.exitCode || 1);
  242. });
  243. }