123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- import * as minimist from 'minimist';
- import * as streamChain from 'stream-chain';
- import * as streamJson from 'stream-json';
- import { ignore as streamJsonIgnore } from 'stream-json/filters/Ignore';
- import { streamArray as streamJsonStreamArray } from 'stream-json/streamers/StreamArray';
- import * as childProcess from 'node:child_process';
- import * as fs from 'node:fs';
- import * as os from 'node:os';
- import * as path from 'node:path';
- import { chunkFilenames, findMatchingFiles } from './lib/utils';
- const SOURCE_ROOT = path.normalize(path.dirname(__dirname));
- const LLVM_BIN = path.resolve(
- SOURCE_ROOT,
- '..',
- 'third_party',
- 'llvm-build',
- 'Release+Asserts',
- 'bin'
- );
- const PLATFORM = os.platform();
- type SpawnAsyncResult = {
- stdout: string;
- stderr: string;
- status: number | null;
- };
- class ErrorWithExitCode extends Error {
- exitCode: number;
- constructor (message: string, exitCode: number) {
- super(message);
- this.exitCode = exitCode;
- }
- }
- async function spawnAsync (
- command: string,
- args: string[],
- options?: childProcess.SpawnOptionsWithoutStdio | undefined
- ): Promise<SpawnAsyncResult> {
- return new Promise((resolve, reject) => {
- try {
- const stdio = { stdout: '', stderr: '' };
- const spawned = childProcess.spawn(command, args, options || {});
- spawned.stdout.on('data', (data) => {
- stdio.stdout += data;
- });
- spawned.stderr.on('data', (data) => {
- stdio.stderr += data;
- });
- spawned.on('exit', (code) => resolve({ ...stdio, status: code }));
- spawned.on('error', (err) => reject(err));
- } catch (err) {
- reject(err);
- }
- });
- }
- function getDepotToolsEnv (): NodeJS.ProcessEnv {
- let depotToolsEnv;
- const findDepotToolsOnPath = () => {
- const result = childProcess.spawnSync(
- PLATFORM === 'win32' ? 'where' : 'which',
- ['gclient']
- );
- if (result.status === 0) {
- return process.env;
- }
- };
- const checkForBuildTools = () => {
- const result = childProcess.spawnSync(
- 'electron-build-tools',
- ['show', 'env', '--json'],
- { shell: true }
- );
- if (result.status === 0) {
- return {
- ...process.env,
- ...JSON.parse(result.stdout.toString().trim())
- };
- }
- };
- try {
- depotToolsEnv = findDepotToolsOnPath();
- if (!depotToolsEnv) depotToolsEnv = checkForBuildTools();
- } catch {}
- if (!depotToolsEnv) {
- throw new Error("Couldn't find depot_tools, ensure it's on your PATH");
- }
- if (!('CHROMIUM_BUILDTOOLS_PATH' in depotToolsEnv)) {
- throw new Error(
- 'CHROMIUM_BUILDTOOLS_PATH environment variable must be set'
- );
- }
- return depotToolsEnv;
- }
- async function runClangTidy (
- outDir: string,
- filenames: string[],
- checks: string = '',
- jobs: number = 1
- ): Promise<boolean> {
- const cmd = path.resolve(LLVM_BIN, 'clang-tidy');
- const args = [`-p=${outDir}`, '--use-color'];
- if (checks) args.push(`--checks=${checks}`);
- // Remove any files that aren't in the compilation database to prevent
- // errors from cluttering up the output. Since the compilation DB is hundreds
- // of megabytes, this is done with streaming to not hold it all in memory.
- const filterCompilationDatabase = (): Promise<string[]> => {
- const compiledFilenames: string[] = [];
- return new Promise((resolve) => {
- const pipeline = streamChain.chain([
- fs.createReadStream(path.resolve(outDir, 'compile_commands.json')),
- streamJson.parser(),
- streamJsonIgnore({ filter: /\bcommand\b/i }),
- streamJsonStreamArray(),
- ({ value: { file, directory } }) => {
- const filename = path.resolve(directory, file);
- return filenames.includes(filename) ? filename : null;
- }
- ]);
- pipeline.on('data', (data) => compiledFilenames.push(data));
- pipeline.on('end', () => resolve(compiledFilenames));
- });
- };
- // clang-tidy can figure out the file from a short relative filename, so
- // to get the most bang for the buck on the command line, let's trim the
- // filenames to the minimum so that we can fit more per invocation
- filenames = (await filterCompilationDatabase()).map((filename) =>
- path.relative(SOURCE_ROOT, filename)
- );
- if (filenames.length === 0) {
- throw new Error('No filenames to run');
- }
- const commandLength =
- cmd.length + args.reduce((length, arg) => length + arg.length, 0);
- const results: boolean[] = [];
- const asyncWorkers = [];
- const chunkedFilenames: string[][] = [];
- const filesPerWorker = Math.ceil(filenames.length / jobs);
- for (let i = 0; i < jobs; i++) {
- chunkedFilenames.push(
- ...chunkFilenames(filenames.splice(0, filesPerWorker), commandLength)
- );
- }
- const worker = async () => {
- let filenames = chunkedFilenames.shift();
- while (filenames?.length) {
- results.push(
- await spawnAsync(cmd, [...args, ...filenames], {}).then((result) => {
- console.log(result.stdout);
- if (result.status !== 0) {
- console.error(result.stderr);
- }
- // On a clean run there's nothing on stdout. A run with warnings-only
- // will have a status code of zero, but there's output on stdout
- return result.status === 0 && result.stdout.length === 0;
- })
- );
- filenames = chunkedFilenames.shift();
- }
- };
- for (let i = 0; i < jobs; i++) {
- asyncWorkers.push(worker());
- }
- try {
- await Promise.all(asyncWorkers);
- return results.every((x) => x);
- } catch {
- return false;
- }
- }
- function parseCommandLine () {
- const showUsage = (arg?: string) : boolean => {
- if (!arg || arg.startsWith('-')) {
- console.log(
- 'Usage: script/run-clang-tidy.ts [-h|--help] [--jobs|-j] ' +
- '[--checks] --out-dir OUTDIR [file1 file2]'
- );
- process.exit(0);
- }
- return true;
- };
- const opts = minimist(process.argv.slice(2), {
- boolean: ['help'],
- string: ['checks', 'out-dir'],
- default: { jobs: 1 },
- alias: { help: 'h', jobs: 'j' },
- stopEarly: true,
- unknown: showUsage
- });
- if (opts.help) showUsage();
- if (!opts['out-dir']) {
- console.log('--out-dir is a required argument');
- process.exit(0);
- }
- return opts;
- }
- async function main (): Promise<boolean> {
- const opts = parseCommandLine();
- const outDir = path.resolve(opts['out-dir']);
- if (!fs.existsSync(outDir)) {
- throw new Error("Output directory doesn't exist");
- } else {
- // Make sure the compile_commands.json file is up-to-date
- const env = getDepotToolsEnv();
- const result = childProcess.spawnSync(
- 'gn',
- ['gen', '.', '--export-compile-commands'],
- { cwd: outDir, env, shell: true }
- );
- if (result.status !== 0) {
- if (result.error) {
- console.error(result.error.message);
- } else {
- console.error(result.stderr.toString());
- }
- throw new ErrorWithExitCode(
- 'Failed to automatically generate compile_commands.json for ' +
- 'output directory',
- 2
- );
- }
- }
- const filenames = [];
- if (opts._.length > 0) {
- filenames.push(...opts._.map((filename) => path.resolve(filename)));
- } else {
- filenames.push(
- ...(await findMatchingFiles(
- path.resolve(SOURCE_ROOT, 'shell'),
- (filename: string) => /.*\.(?:cc|h|mm)$/.test(filename)
- ))
- );
- }
- return runClangTidy(outDir, filenames, opts.checks, opts.jobs);
- }
- if (require.main === module) {
- main()
- .then((success) => {
- process.exit(success ? 0 : 1);
- })
- .catch((err: ErrorWithExitCode) => {
- console.error(`ERROR: ${err.message}`);
- process.exit(err.exitCode || 1);
- });
- }
|