blob: 16d130413c8a2bcf352fc4224d28241c79c7cfcb [file] [log] [blame]
Shuhei Takahashiaaaef622025-05-15 05:30:321#!/usr/bin/env python3
2# Copyright 2025 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""The stable API endpoint for ChromiumIDE Java language support.
6
7ChromiumIDE executes this script to query information and perform operations.
8This script is not meant to be run manually.
9"""
10
11import argparse
12import concurrent.futures
13import dataclasses
14import logging
15import json
16import os
17import re
18import shlex
19import shutil
20import subprocess
21import sys
Shuhei Takahashibfd5bc3a2025-05-16 05:03:0122from typing import Iterator, List, Optional, Set, Tuple
Shuhei Takahashiaaaef622025-05-15 05:30:3223
Shuhei Takahashibfd5bc3a2025-05-16 05:03:0124_SRC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
Shuhei Takahashiaaaef622025-05-15 05:30:3225
26sys.path.append(os.path.join(_SRC_ROOT, 'build'))
27import gn_helpers
28
29sys.path.append(os.path.join(_SRC_ROOT, 'build', 'android', 'gyp'))
30from util import build_utils
31
32_DEPOT_TOOLS_PATH = os.path.join(_SRC_ROOT, 'third_party', 'depot_tools')
33
34# The API version of the script.
35_API_VERSION = 1
36
37# Matches with the package declaration in a Java source file.
38_PACKAGE_PATTERN = re.compile(r'package\s+([A-Za-z0-9._]*)\s*;')
39
40
41@dataclasses.dataclass(frozen=True)
42class BuildInfo:
43 """Defines the output schema of build-info subcommand."""
44 # Directory paths containing Java source files, relative from the src
45 # directory.
46 source_paths: List[str]
47
48 # JAR file paths, relative from the src directory.
49 class_paths: List[str]
50
51 def to_dict(self) -> dict:
52 """Converts the BuildInfo object to a dictionary for JSON serialization."""
53 return {'sourcePaths': self.source_paths, 'classPaths': self.class_paths}
54
55
56def _gn_gen(output_dir: str) -> None:
57 """Runs 'gn gen' to generate build files for the specified output directory.
58
59 Args:
60 output_dir: The path to the build output directory.
61 """
Peter Wen6682c122025-06-11 21:50:5662 cmd = [
63 sys.executable,
64 os.path.join(_DEPOT_TOOLS_PATH, 'gn.py'), 'gen', output_dir
65 ]
Shuhei Takahashiaaaef622025-05-15 05:30:3266 logging.info('Running: %s', shlex.join(cmd))
67 subprocess.check_call(cmd, stdout=sys.stderr)
68
69
70def _compile(output_dir: str, args: List[str]) -> None:
71 """Compiles the specified targets using the build system.
72
73 Args:
74 output_dir: The path to the build output directory.
75 args: A list of build targets or arguments to pass to the build command.
76 """
77 cmd = gn_helpers.CreateBuildCommand(output_dir) + args
78 logging.info('Running: %s', shlex.join(cmd))
79 subprocess.check_call(cmd, stdout=sys.stderr)
80
81
82def _is_useful_source_jar(source_jar_path: str, output_dir: str) -> bool:
83 """Determines if a source JAR is useful for IDE indexing.
84
85 This function filters out certain types of source JARs that are not
86 beneficial or could cause issues for IDEs.
87
88 Args:
89 source_jar_path: The path to the source JAR file.
90 output_dir: The path to the build output directory.
91
92 Returns:
93 True if the source JAR should be included, False otherwise.
94 """
95 # JNI placeholder srcjars contain random stubs.
96 if source_jar_path.endswith('_placeholder.srcjar'):
97 return False
98
99 # When building a java_library target (e.g. //chrome/android:chrome_java),
100 # a srcjar containing relevant resource definitions are generated first (e.g.
101 # gen/chrome/android/chrome_java__assetres.srcjar), and it's included in the
102 # source path when building the java_library target. This is how R references
103 # are resolved when *.java are compiled with javac.
104 #
105 # When multiple java libraries are linked into an APK (e.g.
106 # //clank/java:chrome_apk), another srcjar containing all relevant resource
107 # definitions is generated (e.g.
108 # gen/clank/java/chrome_apk__compile_resources.srcjar). Note that
109 # *__assetres.srcjar used on compiling java libraries are NOT linked to final
110 # APKs. This is how R definitions looked up in the run time are linked to an
111 # APK.
112 #
113 # Then let's talk about IDE's business. We cannot add *__assetres.srcjar to
114 # the source path of the language server because many of those srcjars
115 # contain identically named R classes (e.g. org.chromium.chrome.R) with
116 # different sets of resource names. (Note that we don't explicitly exclude
117 # them here, but actually they don't appear in *.params.json at all.)
118 # Therefore we want to pick a single resource jar that covers all resource
119 # definitions in the whole Chromium tree and add it to the source path. An
120 # approximation used here is to pick the __compile_resources.srcjar for the
121 # main browser binary. This is not perfect though, because there can be some
122 # resources not linked into the main browser binary. Ideally we should
123 # introduce a GN target producing a resource jar covering all resources
124 # across the repository.
125 if source_jar_path.endswith('__compile_resources.srcjar'):
126 if os.path.exists(os.path.join(_SRC_ROOT, 'clank')):
127 private_resources_jar_path = os.path.join(
128 output_dir, 'gen/clank/java/chrome_apk__compile_resources.srcjar')
129 return source_jar_path == private_resources_jar_path
130
131 public_resources_jar_path = os.path.join(
132 output_dir,
133 'gen/chrome/android/chrome_public_apk__compile_resources.srcjar')
134 return source_jar_path == public_resources_jar_path
135
136 return True
137
138
139def _find_source_root(source_file: str) -> Optional[str]:
140 """Finds the root directory for a given source file based on its package.
141
142 For example, if a file '/path/to/src/org/chromium/foo/Bar.java' declares
143 'package org.chromium.foo;', this function will return '/path/to/src'.
144
145 Args:
146 source_file: The path to the Java source file.
147
148 Returns:
149 The path to the source root, or None if the package declaration cannot be
150 found or parsed.
151 """
152 with open(source_file) as f:
153 for line in f:
154 if match := _PACKAGE_PATTERN.match(line):
155 package_name = match.group(1)
156 break
157 else:
158 return None
159
160 depth = package_name.count('.') + 1
161 source_root = source_file.rsplit('/', depth + 1)[0]
162 return source_root
163
164
165def _process_sources(source_files: List[str], output_dir: str,
166 source_path_set: Set[str]) -> None:
167 """Processes a list of source files to find their source roots.
168
169 Args:
170 source_files: A list of source file paths, relative to the output directory.
171 output_dir: The path to the build output directory.
172 source_path_set: A set to which identified source root paths will be added.
173 """
174 processed_dir_set = set()
175 for source_file in source_files:
176 if not source_file.endswith('.java') or not source_file.startswith('../'):
177 continue
178
179 source_file = os.path.normpath(os.path.join(output_dir, source_file))
180 source_dir = os.path.dirname(source_file)
181
182 if source_dir in processed_dir_set:
183 continue
184 processed_dir_set.add(source_dir)
185
186 if source_root := _find_source_root(source_file):
187 source_path_set.add(source_root)
188
189
190def _process_params(params_path: str, output_dir: str,
191 source_path_set: Set[str], class_path_set: Set[str],
192 source_jar_set: Set[str]) -> None:
193 """Processes a .params.json file to extract build information.
194
195 .params.json files are generated on `gn gen` and contain metadata about build
196 targets, including sources, dependencies, and generated JARs.
197
198 Args:
199 params_path: The path to the .params.json file.
200 output_dir: The path to the build output directory.
201 source_path_set: A set to which identified source root paths will be added.
202 class_path_set: A set to which identified classpath JAR paths will be added.
203 source_jar_set: A set to which identified source JAR paths will be added.
204 """
205 with open(params_path) as f:
206 params = json.load(f)
207
208 if target_sources_file := params.get('target_sources_file'):
209 # If the target is built from source files, add their source roots.
210 _process_sources(
211 build_utils.ReadSourcesList(
212 os.path.join(output_dir, target_sources_file)), output_dir,
213 source_path_set)
214 elif unprocessed_jar_path := params.get('unprocessed_jar_path'):
215 # If is_prebuilt is not set, we guess it from the jar path. The path is
216 # relative to outDir, so it starts with ../ if it points to a prebuilt
217 # jar in the chrome source tree.
218 if params.get('is_prebuilt') or unprocessed_jar_path.startswith('../'):
219 # This is a prebuilt jar file. Add it to the class path.
220 class_path_set.add(
221 os.path.normpath(os.path.join(output_dir, unprocessed_jar_path)))
222
223 source_jar_relative_paths = params.get('bundled_srcjars', [])
224 for source_jar_relative_path in source_jar_relative_paths:
225 if not source_jar_relative_path.startswith('gen/'):
226 continue
227 source_jar_path = os.path.join(output_dir, source_jar_relative_path)
228 if _is_useful_source_jar(source_jar_path, output_dir):
229 source_jar_set.add(source_jar_path)
230
231
Shuhei Takahashibfd5bc3a2025-05-16 05:03:01232def _find_params(output_dir: str) -> Iterator[str]:
233 """Finds all .params.json files within the output directory.
234
235 It uses list_java_targets.py to enumerate .params.json files, correctly
236 ignoring stale ones in the output directory.
237
238 Args:
239 output_dir: The path to the build output directory.
240
241 Yields:
242 The paths to the .params.json files.
243 """
244 output = subprocess.check_output(
245 [
246 os.path.join(_SRC_ROOT, 'build', 'android', 'list_java_targets.py'),
247 '--output-directory=' + output_dir,
Andrew Grieve8def3982025-06-09 15:47:13248 '--omit-targets',
Shuhei Takahashibfd5bc3a2025-05-16 05:03:01249 '--print-params-paths',
250 ],
251 cwd=_SRC_ROOT,
252 encoding='utf-8',
253 )
Andrew Grieve8def3982025-06-09 15:47:13254 return output.splitlines()
Shuhei Takahashibfd5bc3a2025-05-16 05:03:01255
256
Shuhei Takahashiaaaef622025-05-15 05:30:32257def _scan_params(output_dir: str) -> Tuple[List[str], List[str], List[str]]:
258 """Scans the output directory for .params.json files and processes them.
259
260 This function walks through the 'gen' subdirectory of the output directory
261 to find all .params.json files and extracts source paths, class paths,
262 and source JARs from them.
263
264 Args:
265 output_dir: The path to the build output directory.
266
267 Returns:
268 A tuple containing:
269 - A sorted list of source root directory paths.
270 - A sorted list of classpath JAR file paths.
271 - A sorted list of source JAR file paths.
272 """
273 source_path_set: Set[str] = set()
274 class_path_set: Set[str] = set()
275 source_jar_set: Set[str] = set()
276
Shuhei Takahashibfd5bc3a2025-05-16 05:03:01277 for params_path in _find_params(output_dir):
278 _process_params(params_path, output_dir, source_path_set, class_path_set,
279 source_jar_set)
Shuhei Takahashiaaaef622025-05-15 05:30:32280
281 return sorted(source_path_set), sorted(class_path_set), sorted(source_jar_set)
282
283
284def _extract_source_jar(source_jar: str) -> str:
285 """Extracts a source JAR file to a directory.
286
287 The extraction is skipped if the JAR has not been modified since the last
288 extraction.
289
290 Args:
291 source_jar: The path to the source JAR file.
292
293 Returns:
294 The path to the directory where the JAR was extracted.
295 """
296 extract_dir = source_jar + '.extracted-for-vscode'
297
298 # Compare timestamps to avoid extracting source jars on every startup.
299 source_jar_mtime = os.stat(source_jar).st_mtime
300 try:
301 extract_dir_mtime = os.stat(extract_dir).st_mtime
302 except OSError:
303 extract_dir_mtime = 0
304 if source_jar_mtime <= extract_dir_mtime:
305 return extract_dir
306
307 logging.info('Extracting %s', source_jar)
308
309 os.makedirs(extract_dir, exist_ok=True)
310
311 # Use `jar` command from the JDK for optimal performance. Python's zipfile is
312 # not very fast, and suffer from GIL on parallelizing.
313 subprocess.check_call(
314 [
315 os.path.join(_SRC_ROOT, 'third_party', 'jdk', 'current', 'bin',
316 'jar'),
317 '-x',
318 '-f',
319 os.path.abspath(source_jar),
320 ],
321 cwd=extract_dir,
322 stdout=sys.stderr,
323 )
324
325 # Remove org.jni_zero placeholders, if any.
326 jni_zero_dir = os.path.join(extract_dir, 'org', 'jni_zero')
327 if os.path.exists(jni_zero_dir):
328 shutil.rmtree(jni_zero_dir)
329
330 return extract_dir
331
332
333def _extract_source_jars(source_jars: List[str], output_dir: str) -> List[str]:
334 """Extracts a list of source JARs in parallel.
335
336 Before extraction, it ensures that the source JARs themselves are up-to-date
337 by attempting to build them.
338
339 Args:
340 source_jars: A list of paths to source JAR files.
341 output_dir: The path to the build output directory.
342
343 Returns:
344 A sorted list of paths to the directories where the source JARs were
345 extracted.
346 """
347 if not source_jars:
348 return []
349
350 source_jar_targets = [
351 os.path.relpath(source_jar, output_dir) for source_jar in source_jars
352 ]
353 _compile(output_dir, source_jar_targets)
354
355 # Parallelize extracting source JARs as it takes a significant amount of time
356 # to process all JARs for Chromium serially.
357 with concurrent.futures.ThreadPoolExecutor() as executor:
358 new_source_dirs = executor.map(_extract_source_jar, source_jars)
359
360 return sorted(new_source_dirs)
361
362
363def _version_main(_options: argparse.Namespace) -> None:
364 """Handles the 'version' subcommand. Prints the API version."""
365 print(_API_VERSION)
366
367
368def _build_info_main(options: argparse.Namespace) -> None:
369 """Handles the 'build-info' subcommand.
370
371 Gathers build information (source paths, class paths) and prints it
372 as JSON.
373 """
374 _gn_gen(options.output_dir)
375 source_roots, class_jars, source_jars = _scan_params(options.output_dir)
376 source_roots.extend(_extract_source_jars(source_jars, options.output_dir))
377
378 build_info = BuildInfo(
379 source_paths=source_roots,
380 class_paths=class_jars,
381 )
382 json.dump(build_info.to_dict(), sys.stdout, indent=2, sort_keys=True)
383
384
385def _parse_arguments(args: List[str]) -> argparse.Namespace:
386 """Parses command-line arguments for the script."""
387 parser = argparse.ArgumentParser(description=__doc__)
388 subparsers = parser.add_subparsers(dest='subcommand', required=True)
389
390 version_parser = subparsers.add_parser('version', help='Prints version')
391 version_parser.set_defaults(main_func=_version_main)
392
393 build_info_parser = subparsers.add_parser(
394 'build-info', help='Returns information needed to build Java files')
395 build_info_parser.set_defaults(main_func=_build_info_main)
396 build_info_parser.add_argument(
397 '--output-dir',
398 required=True,
399 help='Relative path to the output directory, e.g. "out/Debug"')
400
401 return parser.parse_args(args)
402
403
404def main(args: List[str]) -> None:
405 build_utils.InitLogging('CHROMIUMIDE_API_DEBUG')
406
407 assert os.path.exists('.gn'), 'This script must be run from the src directory'
408
409 options = _parse_arguments(args)
410 options.main_func(options)
411
412
413if __name__ == '__main__':
414 main(sys.argv[1:])