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