File: chromiumide_api.py

package info (click to toggle)
chromium 139.0.7258.127-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,122,156 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (414 lines) | stat: -rwxr-xr-x 14,376 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
#!/usr/bin/env python3
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""The stable API endpoint for ChromiumIDE Java language support.

ChromiumIDE executes this script to query information and perform operations.
This script is not meant to be run manually.
"""

import argparse
import concurrent.futures
import dataclasses
import logging
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
from typing import Iterator, List, Optional, Set, Tuple

_SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))

sys.path.append(os.path.join(_SRC_ROOT, 'build'))
import gn_helpers

sys.path.append(os.path.join(_SRC_ROOT, 'build', 'android', 'gyp'))
from util import build_utils

_DEPOT_TOOLS_PATH = os.path.join(_SRC_ROOT, 'third_party', 'depot_tools')

# The API version of the script.
_API_VERSION = 1

# Matches with the package declaration in a Java source file.
_PACKAGE_PATTERN = re.compile(r'package\s+([A-Za-z0-9._]*)\s*;')


@dataclasses.dataclass(frozen=True)
class BuildInfo:
  """Defines the output schema of build-info subcommand."""
  # Directory paths containing Java source files, relative from the src
  # directory.
  source_paths: List[str]

  # JAR file paths, relative from the src directory.
  class_paths: List[str]

  def to_dict(self) -> dict:
    """Converts the BuildInfo object to a dictionary for JSON serialization."""
    return {'sourcePaths': self.source_paths, 'classPaths': self.class_paths}


def _gn_gen(output_dir: str) -> None:
  """Runs 'gn gen' to generate build files for the specified output directory.

  Args:
    output_dir: The path to the build output directory.
  """
  cmd = [
      sys.executable,
      os.path.join(_DEPOT_TOOLS_PATH, 'gn.py'), 'gen', output_dir
  ]
  logging.info('Running: %s', shlex.join(cmd))
  subprocess.check_call(cmd, stdout=sys.stderr)


def _compile(output_dir: str, args: List[str]) -> None:
  """Compiles the specified targets using the build system.

  Args:
    output_dir: The path to the build output directory.
    args: A list of build targets or arguments to pass to the build command.
  """
  cmd = gn_helpers.CreateBuildCommand(output_dir) + args
  logging.info('Running: %s', shlex.join(cmd))
  subprocess.check_call(cmd, stdout=sys.stderr)


def _is_useful_source_jar(source_jar_path: str, output_dir: str) -> bool:
  """Determines if a source JAR is useful for IDE indexing.

  This function filters out certain types of source JARs that are not
  beneficial or could cause issues for IDEs.

  Args:
    source_jar_path: The path to the source JAR file.
    output_dir: The path to the build output directory.

  Returns:
    True if the source JAR should be included, False otherwise.
  """
  # JNI placeholder srcjars contain random stubs.
  if source_jar_path.endswith('_placeholder.srcjar'):
    return False

  # When building a java_library target (e.g. //chrome/android:chrome_java),
  # a srcjar containing relevant resource definitions are generated first (e.g.
  # gen/chrome/android/chrome_java__assetres.srcjar), and it's included in the
  # source path when building the java_library target. This is how R references
  # are resolved when *.java are compiled with javac.
  #
  # When multiple java libraries are linked into an APK (e.g.
  # //clank/java:chrome_apk), another srcjar containing all relevant resource
  # definitions is generated (e.g.
  # gen/clank/java/chrome_apk__compile_resources.srcjar). Note that
  # *__assetres.srcjar used on compiling java libraries are NOT linked to final
  # APKs. This is how R definitions looked up in the run time are linked to an
  # APK.
  #
  # Then let's talk about IDE's business. We cannot add *__assetres.srcjar to
  # the source path of the language server because many of those srcjars
  # contain identically named R classes (e.g. org.chromium.chrome.R) with
  # different sets of resource names. (Note that we don't explicitly exclude
  # them here, but actually they don't appear in *.params.json at all.)
  # Therefore we want to pick a single resource jar that covers all resource
  # definitions in the whole Chromium tree and add it to the source path. An
  # approximation used here is to pick the __compile_resources.srcjar for the
  # main browser binary. This is not perfect though, because there can be some
  # resources not linked into the main browser binary. Ideally we should
  # introduce a GN target producing a resource jar covering all resources
  # across the repository.
  if source_jar_path.endswith('__compile_resources.srcjar'):
    if os.path.exists(os.path.join(_SRC_ROOT, 'clank')):
      private_resources_jar_path = os.path.join(
          output_dir, 'gen/clank/java/chrome_apk__compile_resources.srcjar')
      return source_jar_path == private_resources_jar_path

    public_resources_jar_path = os.path.join(
        output_dir,
        'gen/chrome/android/chrome_public_apk__compile_resources.srcjar')
    return source_jar_path == public_resources_jar_path

  return True


def _find_source_root(source_file: str) -> Optional[str]:
  """Finds the root directory for a given source file based on its package.

  For example, if a file '/path/to/src/org/chromium/foo/Bar.java' declares
  'package org.chromium.foo;', this function will return '/path/to/src'.

  Args:
    source_file: The path to the Java source file.

  Returns:
    The path to the source root, or None if the package declaration cannot be
    found or parsed.
  """
  with open(source_file) as f:
    for line in f:
      if match := _PACKAGE_PATTERN.match(line):
        package_name = match.group(1)
        break
    else:
      return None

  depth = package_name.count('.') + 1
  source_root = source_file.rsplit('/', depth + 1)[0]
  return source_root


def _process_sources(source_files: List[str], output_dir: str,
                     source_path_set: Set[str]) -> None:
  """Processes a list of source files to find their source roots.

  Args:
    source_files: A list of source file paths, relative to the output directory.
    output_dir: The path to the build output directory.
    source_path_set: A set to which identified source root paths will be added.
  """
  processed_dir_set = set()
  for source_file in source_files:
    if not source_file.endswith('.java') or not source_file.startswith('../'):
      continue

    source_file = os.path.normpath(os.path.join(output_dir, source_file))
    source_dir = os.path.dirname(source_file)

    if source_dir in processed_dir_set:
      continue
    processed_dir_set.add(source_dir)

    if source_root := _find_source_root(source_file):
      source_path_set.add(source_root)


def _process_params(params_path: str, output_dir: str,
                    source_path_set: Set[str], class_path_set: Set[str],
                    source_jar_set: Set[str]) -> None:
  """Processes a .params.json file to extract build information.

  .params.json files are generated on `gn gen` and contain metadata about build
  targets, including sources, dependencies, and generated JARs.

  Args:
    params_path: The path to the .params.json file.
    output_dir: The path to the build output directory.
    source_path_set: A set to which identified source root paths will be added.
    class_path_set: A set to which identified classpath JAR paths will be added.
    source_jar_set: A set to which identified source JAR paths will be added.
  """
  with open(params_path) as f:
    params = json.load(f)

  if target_sources_file := params.get('target_sources_file'):
    # If the target is built from source files, add their source roots.
    _process_sources(
        build_utils.ReadSourcesList(
            os.path.join(output_dir, target_sources_file)), output_dir,
        source_path_set)
  elif unprocessed_jar_path := params.get('unprocessed_jar_path'):
    # If is_prebuilt is not set, we guess it from the jar path. The path is
    # relative to outDir, so it starts with ../ if it points to a prebuilt
    # jar in the chrome source tree.
    if params.get('is_prebuilt') or unprocessed_jar_path.startswith('../'):
      # This is a prebuilt jar file. Add it to the class path.
      class_path_set.add(
          os.path.normpath(os.path.join(output_dir, unprocessed_jar_path)))

  source_jar_relative_paths = params.get('bundled_srcjars', [])
  for source_jar_relative_path in source_jar_relative_paths:
    if not source_jar_relative_path.startswith('gen/'):
      continue
    source_jar_path = os.path.join(output_dir, source_jar_relative_path)
    if _is_useful_source_jar(source_jar_path, output_dir):
      source_jar_set.add(source_jar_path)


def _find_params(output_dir: str) -> Iterator[str]:
  """Finds all .params.json files within the output directory.

  It uses list_java_targets.py to enumerate .params.json files, correctly
  ignoring stale ones in the output directory.

  Args:
    output_dir: The path to the build output directory.

  Yields:
    The paths to the .params.json files.
  """
  output = subprocess.check_output(
      [
          os.path.join(_SRC_ROOT, 'build', 'android', 'list_java_targets.py'),
          '--output-directory=' + output_dir,
          '--omit-targets',
          '--print-params-paths',
      ],
      cwd=_SRC_ROOT,
      encoding='utf-8',
  )
  return output.splitlines()


def _scan_params(output_dir: str) -> Tuple[List[str], List[str], List[str]]:
  """Scans the output directory for .params.json files and processes them.

  This function walks through the 'gen' subdirectory of the output directory
  to find all .params.json files and extracts source paths, class paths,
  and source JARs from them.

  Args:
    output_dir: The path to the build output directory.

  Returns:
    A tuple containing:
      - A sorted list of source root directory paths.
      - A sorted list of classpath JAR file paths.
      - A sorted list of source JAR file paths.
  """
  source_path_set: Set[str] = set()
  class_path_set: Set[str] = set()
  source_jar_set: Set[str] = set()

  for params_path in _find_params(output_dir):
    _process_params(params_path, output_dir, source_path_set, class_path_set,
                    source_jar_set)

  return sorted(source_path_set), sorted(class_path_set), sorted(source_jar_set)


def _extract_source_jar(source_jar: str) -> str:
  """Extracts a source JAR file to a directory.

  The extraction is skipped if the JAR has not been modified since the last
  extraction.

  Args:
    source_jar: The path to the source JAR file.

  Returns:
    The path to the directory where the JAR was extracted.
  """
  extract_dir = source_jar + '.extracted-for-vscode'

  # Compare timestamps to avoid extracting source jars on every startup.
  source_jar_mtime = os.stat(source_jar).st_mtime
  try:
    extract_dir_mtime = os.stat(extract_dir).st_mtime
  except OSError:
    extract_dir_mtime = 0
  if source_jar_mtime <= extract_dir_mtime:
    return extract_dir

  logging.info('Extracting %s', source_jar)

  os.makedirs(extract_dir, exist_ok=True)

  # Use `jar` command from the JDK for optimal performance. Python's zipfile is
  # not very fast, and suffer from GIL on parallelizing.
  subprocess.check_call(
      [
          os.path.join(_SRC_ROOT, 'third_party', 'jdk', 'current', 'bin',
                       'jar'),
          '-x',
          '-f',
          os.path.abspath(source_jar),
      ],
      cwd=extract_dir,
      stdout=sys.stderr,
  )

  # Remove org.jni_zero placeholders, if any.
  jni_zero_dir = os.path.join(extract_dir, 'org', 'jni_zero')
  if os.path.exists(jni_zero_dir):
    shutil.rmtree(jni_zero_dir)

  return extract_dir


def _extract_source_jars(source_jars: List[str], output_dir: str) -> List[str]:
  """Extracts a list of source JARs in parallel.

  Before extraction, it ensures that the source JARs themselves are up-to-date
  by attempting to build them.

  Args:
    source_jars: A list of paths to source JAR files.
    output_dir: The path to the build output directory.

  Returns:
    A sorted list of paths to the directories where the source JARs were
    extracted.
  """
  if not source_jars:
    return []

  source_jar_targets = [
      os.path.relpath(source_jar, output_dir) for source_jar in source_jars
  ]
  _compile(output_dir, source_jar_targets)

  # Parallelize extracting source JARs as it takes a significant amount of time
  # to process all JARs for Chromium serially.
  with concurrent.futures.ThreadPoolExecutor() as executor:
    new_source_dirs = executor.map(_extract_source_jar, source_jars)

  return sorted(new_source_dirs)


def _version_main(_options: argparse.Namespace) -> None:
  """Handles the 'version' subcommand. Prints the API version."""
  print(_API_VERSION)


def _build_info_main(options: argparse.Namespace) -> None:
  """Handles the 'build-info' subcommand.

  Gathers build information (source paths, class paths) and prints it
  as JSON.
  """
  _gn_gen(options.output_dir)
  source_roots, class_jars, source_jars = _scan_params(options.output_dir)
  source_roots.extend(_extract_source_jars(source_jars, options.output_dir))

  build_info = BuildInfo(
      source_paths=source_roots,
      class_paths=class_jars,
  )
  json.dump(build_info.to_dict(), sys.stdout, indent=2, sort_keys=True)


def _parse_arguments(args: List[str]) -> argparse.Namespace:
  """Parses command-line arguments for the script."""
  parser = argparse.ArgumentParser(description=__doc__)
  subparsers = parser.add_subparsers(dest='subcommand', required=True)

  version_parser = subparsers.add_parser('version', help='Prints version')
  version_parser.set_defaults(main_func=_version_main)

  build_info_parser = subparsers.add_parser(
      'build-info', help='Returns information needed to build Java files')
  build_info_parser.set_defaults(main_func=_build_info_main)
  build_info_parser.add_argument(
      '--output-dir',
      required=True,
      help='Relative path to the output directory, e.g. "out/Debug"')

  return parser.parse_args(args)


def main(args: List[str]) -> None:
  build_utils.InitLogging('CHROMIUMIDE_API_DEBUG')

  assert os.path.exists('.gn'), 'This script must be run from the src directory'

  options = _parse_arguments(args)
  options.main_func(options)


if __name__ == '__main__':
  main(sys.argv[1:])