1#!/usr/bin/env vpython3 2 3# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 4# 5# Use of this source code is governed by a BSD-style license 6# that can be found in the LICENSE file in the root of the source 7# tree. An additional intellectual property rights grant can be found 8# in the file PATENTS. All contributing project authors may 9# be found in the AUTHORS file in the root of the source tree. 10"""This script helps to invoke gn and ninja 11which lie in depot_tools repository.""" 12 13import json 14import os 15import re 16import shutil 17import subprocess 18import sys 19import tempfile 20 21 22def FindSrcDirPath(): 23 """Returns the abs path to the src/ dir of the project.""" 24 src_dir = os.path.dirname(os.path.abspath(__file__)) 25 while os.path.basename(src_dir) != 'src': 26 src_dir = os.path.normpath(os.path.join(src_dir, os.pardir)) 27 return src_dir 28 29 30SRC_DIR = FindSrcDirPath() 31sys.path.append(os.path.join(SRC_DIR, 'build')) 32import find_depot_tools 33 34 35def RunGnCommand(args, root_dir=None): 36 """Runs `gn` with provided args and return error if any.""" 37 try: 38 command = [ 39 sys.executable, 40 os.path.join(find_depot_tools.DEPOT_TOOLS_PATH, 'gn.py') 41 ] + args 42 subprocess.check_output(command, cwd=root_dir) 43 except subprocess.CalledProcessError as err: 44 return err.output 45 return None 46 47 48# GN_ERROR_RE matches the summary of an error output by `gn check`. 49# Matches "ERROR" and following lines until it sees an empty line or a line 50# containing just underscores. 51GN_ERROR_RE = re.compile(r'^ERROR .+(?:\n.*[^_\n].*$)+', re.MULTILINE) 52 53 54def RunGnCheck(root_dir=None): 55 """Runs `gn gen --check` with default args to detect mismatches between 56 #includes and dependencies in the BUILD.gn files, as well as general build 57 errors. 58 59 Returns a list of error summary strings. 60 """ 61 out_dir = tempfile.mkdtemp('gn') 62 try: 63 error = RunGnCommand(['gen', '--check', out_dir], root_dir) 64 finally: 65 shutil.rmtree(out_dir, ignore_errors=True) 66 return GN_ERROR_RE.findall(error.decode('utf-8')) if error else [] 67 68 69def RunNinjaCommand(args, root_dir=None): 70 """Runs ninja quietly. Any failure (e.g. clang not found) is 71 silently discarded, since this is unlikely an error in submitted CL.""" 72 command = [os.path.join(find_depot_tools.DEPOT_TOOLS_PATH, 'ninja')] + args 73 p = subprocess.Popen(command, 74 cwd=root_dir, 75 stdout=subprocess.PIPE, 76 stderr=subprocess.PIPE) 77 out, _ = p.communicate() 78 return out 79 80 81def GetClangTidyPath(): 82 """POC/WIP! Use the one we have, even it doesn't match clang's version.""" 83 tidy = ('third_party/android_ndk/toolchains/' 84 'llvm/prebuilt/linux-x86_64/bin/clang-tidy') 85 return os.path.join(SRC_DIR, tidy) 86 87 88def GetCompilationDb(root_dir=None): 89 """Run ninja compdb tool to get proper flags, defines and include paths.""" 90 # The compdb tool expect a rule. 91 commands = json.loads(RunNinjaCommand(['-t', 'compdb', 'cxx'], root_dir)) 92 # Turns 'file' field into a key. 93 return {v['file']: v for v in commands} 94 95 96def GetCompilationCommand(filepath, gn_args, work_dir): 97 """Get the whole command used to compile one cc file. 98 Typically, clang++ with flags, defines and include paths. 99 100 Args: 101 filepath: path to .cc file. 102 gen_args: build configuration for gn. 103 work_dir: build dir. 104 105 Returns: 106 Command as a list, ready to be consumed by subprocess.Popen. 107 """ 108 gn_errors = RunGnCommand(['gen'] + gn_args + [work_dir]) 109 if gn_errors: 110 raise RuntimeError('FYI, cannot complete check due to gn error:\n%s\n' 111 'Please open a bug.' % gn_errors) 112 113 # Needed for single file compilation. 114 commands = GetCompilationDb(work_dir) 115 116 # Path as referenced by ninja. 117 rel_path = os.path.relpath(os.path.abspath(filepath), work_dir) 118 119 # Gather defines, include path and flags (such as -std=c++11). 120 try: 121 compilation_entry = commands[rel_path] 122 except KeyError as not_found: 123 raise ValueError('%s: Not found in compilation database.\n' 124 'Please check the path.' % filepath) from not_found 125 command = compilation_entry['command'].split() 126 127 # Remove troublesome flags. May trigger an error otherwise. 128 if '-MMD' in command: 129 command.remove('-MMD') 130 if '-MF' in command: 131 index = command.index('-MF') 132 del command[index:index + 2] # Remove filename as well. 133 134 return command 135