"""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')
_API_VERSION = 1
_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."""
source_paths: List[str]
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.
"""
if source_jar_path.endswith('_placeholder.srcjar'):
return False
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'):
_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 params.get('is_prebuilt') or unprocessed_jar_path.startswith('../'):
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'
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)
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,
)
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)
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:])