#!/usr/bin/env vpython3 # Copyright 2023 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Given a .build_config.json file, generates a .classpath file that can be used with the "Language Support for Java™ by Red Hat" Visual Studio Code extension. See //docs/vscode.md for details. """ import argparse import logging import json import os import sys import xml.etree.ElementTree sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp')) from util import build_utils sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) import gn_helpers def _WithoutSuffix(string, suffix): if not string.endswith(suffix): raise ValueError(f'{string!r} does not end with {suffix!r}') return string[:-len(suffix)] def _GetJavaRoot(path): # The authoritative way to determine the Java root for a given source file is # to parse the source code and extract the package and class names, but let's # keep things simple and use some heuristics to try to guess the Java root # from the file path instead. while True: dirname, basename = os.path.split(path) if not basename: raise RuntimeError(f'Unable to determine the Java root for {path!r}') if basename in ('java', 'src'): return path if basename in ('javax', 'org', 'com'): return dirname path = dirname def _ProcessSourceFile(output_dir, source_file_path, source_dirs): source_file_path = os.path.normpath(os.path.join(output_dir, source_file_path)) java_root = _GetJavaRoot(source_file_path) logging.debug('Extracted java root `%s` from source file path `%s`', java_root, source_file_path) source_dirs.add(java_root) def _ProcessSourcesFile(output_dir, sources_file_path, source_dirs): for source_file_path in build_utils.ReadSourcesList( os.path.join(output_dir, sources_file_path)): _ProcessSourceFile(output_dir, source_file_path, source_dirs) def _ProcessBuildConfigFile(output_dir, build_config_path, source_dirs, libs, already_processed_build_config_files, android_sdk_build_tools_version): if build_config_path in already_processed_build_config_files: return already_processed_build_config_files.add(build_config_path) logging.info('Processing build config: %s', build_config_path) with open(os.path.join(output_dir, build_config_path)) as build_config_file: build_config = json.load(build_config_file) deps_info = build_config['deps_info'] target_sources_file = deps_info.get('target_sources_file') if target_sources_file is not None: _ProcessSourcesFile(output_dir, target_sources_file, source_dirs) else: unprocessed_jar_path = deps_info.get('unprocessed_jar_path') if unprocessed_jar_path is not None: lib_path = os.path.normpath(os.path.join(output_dir, unprocessed_jar_path)) logging.debug('Found lib `%s', lib_path) libs.add(lib_path) source_dirs.add( os.path.join(output_dir, _WithoutSuffix(build_config_path, '.build_config.json'), 'generated_java', 'input_srcjars')) android = build_config.get('android') if android is not None: # This works around an issue where the language server complains about # `java.lang.invoke.LambdaMetafactory` not being found. The normal Android # build process is fine with this class being missing because d8 removes # references to LambdaMetafactory from the bytecode - see: # https://jakewharton.com/androids-java-8-support/#native-lambdas # When JDT builds the code, d8 doesn't run, so the references are still # there. Fortunately, the Android SDK provides a convenience JAR to fill # that gap in: # //third_party/android_sdk/public/build-tools/*/core-lambda-stubs.jar libs.add( os.path.normpath( os.path.join( output_dir, os.path.dirname(build_config['android']['sdk_jars'][0]), os.pardir, os.pardir, 'build-tools', android_sdk_build_tools_version, 'core-lambda-stubs.jar'))) for dep_config in deps_info['deps_configs']: _ProcessBuildConfigFile(output_dir, dep_config, source_dirs, libs, already_processed_build_config_files, android_sdk_build_tools_version) def _GenerateClasspathEntry(kind, path): classpathentry = xml.etree.ElementTree.Element('classpathentry') classpathentry.set('kind', kind) classpathentry.set('path', f'_/{path}') return classpathentry def _GenerateClasspathFile(source_dirs, libs): classpath = xml.etree.ElementTree.Element('classpath') for source_dir in source_dirs: classpath.append(_GenerateClasspathEntry('src', source_dir)) for lib in libs: classpath.append(_GenerateClasspathEntry('lib', lib)) xml.etree.ElementTree.ElementTree(classpath).write(sys.stdout, encoding='unicode') def _ParseArguments(argv): parser = argparse.ArgumentParser( description= 'Given Chromium Java build config files, dumps an Eclipse JDT classpath ' 'file to standard output that can be used with the "Language Support for ' 'Java™ by Red Hat" Visual Studio Code extension. See //docs/vscode.md ' 'for details.') parser.add_argument( '--output-dir', required=True, help='Relative path to the output directory, e.g. "out/Debug"') parser.add_argument( '--build-config', action='append', required=True, help='Path to the .build_config.json file to use as input, relative to ' '`--output-dir`. May be repeated.') return parser.parse_args(argv) def main(argv): build_utils.InitLogging('GENERATE_VSCODE_CLASSPATH_DEBUG') args = _ParseArguments(argv) output_dir = args.output_dir build_vars = gn_helpers.ReadBuildVars(output_dir) source_dirs = set() libs = set() already_processed_build_config_files = set() for build_config_path in args.build_config: _ProcessBuildConfigFile(output_dir, build_config_path, source_dirs, libs, already_processed_build_config_files, build_vars['android_sdk_build_tools_version']) logging.info('Done processing %d build config files', len(already_processed_build_config_files)) _GenerateClasspathFile(source_dirs, libs) if __name__ == '__main__': sys.exit(main(sys.argv[1:]))