371 lines
		
	
	
	
		
			10 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			371 lines
		
	
	
	
		
			10 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|   | import chalk from 'chalk'; | ||
|  | import * as childProcess from 'child_process'; | ||
|  | import * as fs from 'fs'; | ||
|  | import * as klaw from 'klaw'; | ||
|  | import * as minimist from 'minimist'; | ||
|  | import * as os from 'os'; | ||
|  | import * as path from 'path'; | ||
|  | 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'; | ||
|  | 
 | ||
|  | 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; | ||
|  | } | ||
|  | 
 | ||
|  | function chunkFilenames (filenames: string[], offset: number = 0): string[][] { | ||
|  |   // Windows has a max command line length of 2047 characters, so we can't
 | ||
|  |   // provide too many filenames without going over that. To work around that,
 | ||
|  |   // chunk up a list of filenames such that it won't go over that limit when
 | ||
|  |   // used as args. Use a much higher limit on other platforms which will
 | ||
|  |   // effectively be a no-op.
 | ||
|  |   const MAX_FILENAME_ARGS_LENGTH = | ||
|  |     PLATFORM === 'win32' ? 2047 - offset : 100 * 1024; | ||
|  | 
 | ||
|  |   return filenames.reduce( | ||
|  |     (chunkedFilenames: string[][], filename) => { | ||
|  |       const currChunk = chunkedFilenames[chunkedFilenames.length - 1]; | ||
|  |       const currChunkLength = currChunk.reduce( | ||
|  |         (totalLength, _filename) => totalLength + _filename.length + 1, | ||
|  |         0 | ||
|  |       ); | ||
|  |       if (currChunkLength + filename.length + 1 > MAX_FILENAME_ARGS_LENGTH) { | ||
|  |         chunkedFilenames.push([filename]); | ||
|  |       } else { | ||
|  |         currChunk.push(filename); | ||
|  |       } | ||
|  |       return chunkedFilenames; | ||
|  |     }, | ||
|  |     [[]] | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | 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}`]; | ||
|  | 
 | ||
|  |   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) { | ||
|  |       results.push( | ||
|  |         await spawnAsync(cmd, [...args, ...filenames], {}).then((result) => { | ||
|  |           // We lost color, so recolorize because it's much more legible
 | ||
|  |           // There's a --use-color flag for clang-tidy but it has no effect
 | ||
|  |           // on Windows at the moment, so just recolor for everyone
 | ||
|  |           let state = null; | ||
|  | 
 | ||
|  |           for (const line of result.stdout.split('\n')) { | ||
|  |             if (line.includes(' warning: ')) { | ||
|  |               console.log( | ||
|  |                 line | ||
|  |                   .split(' warning: ') | ||
|  |                   .map((part) => chalk.whiteBright(part)) | ||
|  |                   .join(chalk.magentaBright(' warning: ')) | ||
|  |               ); | ||
|  |               state = 'code-line'; | ||
|  |             } else if (line.includes(' note: ')) { | ||
|  |               const lineParts = line.split(' note: '); | ||
|  |               lineParts[0] = chalk.whiteBright(lineParts[0]); | ||
|  |               console.log(lineParts.join(chalk.grey(' note: '))); | ||
|  |               state = 'code-line'; | ||
|  |             } else if (line.startsWith('error:')) { | ||
|  |               console.log( | ||
|  |                 chalk.redBright('error: ') + line.split(' ').slice(1).join(' ') | ||
|  |               ); | ||
|  |             } else if (state === 'code-line') { | ||
|  |               console.log(line); | ||
|  |               state = 'post-code-line'; | ||
|  |             } else if (state === 'post-code-line') { | ||
|  |               console.log(chalk.greenBright(line)); | ||
|  |             } else { | ||
|  |               console.log(line); | ||
|  |             } | ||
|  |           } | ||
|  | 
 | ||
|  |           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; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | async function findMatchingFiles ( | ||
|  |   top: string, | ||
|  |   test: (filename: string) => boolean | ||
|  | ): Promise<string[]> { | ||
|  |   return new Promise((resolve) => { | ||
|  |     const matches = [] as string[]; | ||
|  |     klaw(top, { | ||
|  |       filter: (f) => path.basename(f) !== '.bin' | ||
|  |     }) | ||
|  |       .on('end', () => resolve(matches)) | ||
|  |       .on('data', (item) => { | ||
|  |         if (test(item.path)) { | ||
|  |           matches.push(item.path); | ||
|  |         } | ||
|  |       }); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | 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 argunment'); | ||
|  |     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); | ||
|  |     }); | ||
|  | } |