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 options = parser.parse_args() 81 82 this_dir = os.path.dirname(__file__) 83 repo_root = os.path.join(this_dir, '..', '..') 84 85 targets = [] 86 use_prepare_header_only = True 87 for source in options.source: 88 _, ext = os.path.splitext(source) 89 if ext not in ('.c', '.cc', '.cxx', '.cpp', '.m', '.mm', '.S', 90 '.h', '.hxx', '.hpp', '.inc'): 91 use_prepare_header_only = False 92 # source is repo root (cwd) relative, 93 # but siso uses out dir relative target. 94 target = os.path.relpath(source, start=options.out_dir) + "^" 95 targets.append(target) 96 97 if _use_reclient(options.out_dir): 98 # b/335795623 ide_query compiler_arguments contain non-compiler arguments 99 sys.stderr.write( 100 'ide_query won\'t work well with "use_reclient=true"\n' 101 'Set "use_reclient=false" in args.gn.\n') 102 sys.exit(1) 103 if options.perform_build: 104 args = ['siso', 'ninja'] 105 # use `-k=0` to build generated files as much as possible. 106 args.extend([ 107 '-k=0', 108 '--prepare', 109 '-C', 110 options.out_dir, 111 ]) 112 if options.log_dir: 113 args.extend(['-log_dir', options.log_dir]) 114 args.extend(targets) 115 env = os.environ.copy() 116 if use_prepare_header_only: 117 env['SISO_EXPERIMENTS'] = 'no-fast-deps,prepare-header-only' 118 else: 119 env['SISO_EXPERIMENTS'] = 'no-fast-deps' 120 with subprocess.Popen( 121 args, 122 cwd=repo_root, 123 env=env, 124 stderr=subprocess.STDOUT, 125 stdout=subprocess.PIPE, 126 universal_newlines=True 127 ) as p: 128 for line in p.stdout: 129 print(line, end='', file=sys.stderr) 130 # loop ends when program finishes, but must wait else returncode is None. 131 p.wait() 132 if p.returncode != 0: 133 # TODO: report error in IdeAnalysis.Status? 134 sys.stderr.write('build failed with %d\n' % p.returncode) 135 # even if build fails, it should report ideanalysis back. 136 137 args = ['siso', 'query', 'ideanalysis', '-C', options.out_dir] 138 args.extend(targets) 139 subprocess.run(args, cwd=repo_root, check=True) 140 141if __name__ == '__main__': 142 sys.exit(main()) 143