• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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