1#!/usr/bin/env python3 2# Copyright 2024 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""A script gets the information needed by lDE language services. 6 7Expected to run it at repository root, where top DEP, .gn etc exists. 8Not intended to run by user. 9See go/reqs-for-peep 10""" 11 12import argparse 13import os 14import re 15import subprocess 16import sys 17 18def _gn_lines(output_dir, path): 19 """ 20 Generator function that returns args.gn lines one at a time, following 21 import directives as needed. 22 """ 23 import_re = re.compile(r'\s*import\("(.*)"\)') 24 with open(path, encoding="utf-8") as f: 25 for line in f: 26 match = import_re.match(line) 27 if match: 28 raw_import_path = match.groups()[0] 29 if raw_import_path[:2] == "//": 30 import_path = os.path.normpath( 31 os.path.join(output_dir, "..", "..", 32 raw_import_path[2:])) 33 else: 34 import_path = os.path.normpath( 35 os.path.join(os.path.dirname(path), raw_import_path)) 36 for import_line in _gn_lines(output_dir, import_path): 37 yield import_line 38 else: 39 yield line 40 41 42def _use_reclient(outdir): 43 use_remoteexec = False 44 use_reclient = None 45 args_gn = os.path.join(outdir, 'args.gn') 46 if not os.path.exists(args_gn): 47 return False 48 for line in _gn_lines(outdir, args_gn): 49 line_without_comment = line.split('#')[0] 50 m = re.match(r"(^|\s*)use_remoteexec\s*=\s*(true|false)\s*$", 51 line_without_comment) 52 if m: 53 use_remoteexec = m.group(2) == 'true' 54 continue 55 m = re.match(r"(^|\s*)use_reclient\s*=\s*(true|false)\s*$", 56 line_without_comment) 57 if m: 58 use_reclient = m.group(2) == 'true' 59 if use_reclient == None: 60 use_reclient = use_remoteexec 61 return use_reclient 62 63 64def main(): 65 parser = argparse.ArgumentParser() 66 parser.add_argument('source', nargs='+', 67 help=('The source file being analyzed.' 68 'Multiple source arguments can be passed in order to batch ' 69 'process if desired.')) 70 parser.add_argument('--perform-build', action='store_true', 71 help=('If specified, actually build the target, including any generated ' 72 'prerequisite files. ' 73 'If --perform-build is not passed, the contents of ' 74 'the GeneratedFile results will only be returned if a build has ' 75 'been previously completed, and may be stale.')) 76 parser.add_argument('--out-dir', 77 help=('Output directory, containing args.gn, which specifies the build ' 78 'configuration.')) 79 parser.add_argument('--log-dir', help=('Directory to save log files to.')) 80 parser.add_argument('--format', choices=['proto', 'prototext', 'json'], 81 default='proto', help=('Output format.')) 82 options = parser.parse_args() 83 84 this_dir = os.path.dirname(__file__) 85 repo_root = os.path.join(this_dir, '..', '..') 86 87 targets = [] 88 use_prepare = True 89 use_prepare_header_only = True 90 for source in options.source: 91 _, ext = os.path.splitext(source) 92 if ext == '.java': 93 # need to include generated *.jar for java. 94 use_prepare = False 95 if ext not in ('.c', '.cc', '.cxx', '.cpp', '.m', '.mm', '.S', 96 '.h', '.hxx', '.hpp', '.inc'): 97 use_prepare_header_only = False 98 # source is repo root (cwd) relative, 99 # but siso uses out dir relative target. 100 target = os.path.relpath(source, start=options.out_dir) + "^" 101 targets.append(target) 102 103 if _use_reclient(options.out_dir): 104 # b/335795623 ide_query compiler_arguments contain non-compiler arguments 105 sys.stderr.write( 106 'ide_query won\'t work well with "use_reclient=true"\n' 107 'Set "use_reclient=false" in args.gn.\n') 108 sys.exit(1) 109 if options.perform_build: 110 # forget last targets of normal build as this build will update 111 # .siso_fs_state. 112 if os.path.exists(os.path.join(options.out_dir, '.siso_last_targets')): 113 os.remove(os.path.join(options.out_dir, '.siso_last_targets')) 114 args = ['siso', 'ninja'] 115 # use `-k=0` to build generated files as much as possible. 116 args.extend([ 117 '-k=0', 118 '-C', 119 options.out_dir, 120 ]) 121 if use_prepare: 122 args.extend(['--prepare']) 123 if options.log_dir: 124 args.extend(['-log_dir', options.log_dir]) 125 args.extend(targets) 126 env = os.environ.copy() 127 if use_prepare_header_only: 128 env['SISO_EXPERIMENTS'] = 'no-fast-deps,prepare-header-only' 129 else: 130 env['SISO_EXPERIMENTS'] = 'no-fast-deps' 131 with subprocess.Popen( 132 args, 133 cwd=repo_root, 134 env=env, 135 stderr=subprocess.STDOUT, 136 stdout=subprocess.PIPE, 137 universal_newlines=True 138 ) as p: 139 for line in p.stdout: 140 print(line, end='', file=sys.stderr) 141 # loop ends when program finishes, but must wait else returncode is None. 142 p.wait() 143 if p.returncode != 0: 144 # TODO: report error in IdeAnalysis.Status? 145 sys.stderr.write('build failed with %d\n' % p.returncode) 146 # even if build fails, it should report ideanalysis back. 147 148 args = ['siso', 'query', 'ideanalysis', '-C', options.out_dir] 149 if options.format: 150 args.extend(['--format', options.format]) 151 args.extend(targets) 152 subprocess.run(args, cwd=repo_root, check=True) 153 154if __name__ == '__main__': 155 sys.exit(main()) 156