1#!/usr/bin/env vpython3 2# Copyright 2023 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"""Given a .build_config.json file, generates a .classpath file that can be 6used with the "Language Support for Java™ by Red Hat" Visual Studio Code 7extension. See //docs/vscode.md for details. 8""" 9 10import argparse 11import logging 12import json 13import os 14import sys 15import xml.etree.ElementTree 16 17sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp')) 18from util import build_utils 19 20sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) 21import gn_helpers 22 23 24def _WithoutSuffix(string, suffix): 25 if not string.endswith(suffix): 26 raise ValueError(f'{string!r} does not end with {suffix!r}') 27 return string[:-len(suffix)] 28 29 30def _GetJavaRoot(path): 31 # The authoritative way to determine the Java root for a given source file is 32 # to parse the source code and extract the package and class names, but let's 33 # keep things simple and use some heuristics to try to guess the Java root 34 # from the file path instead. 35 while True: 36 dirname, basename = os.path.split(path) 37 if not basename: 38 raise RuntimeError(f'Unable to determine the Java root for {path!r}') 39 if basename in ('java', 'src'): 40 return path 41 if basename in ('javax', 'org', 'com'): 42 return dirname 43 path = dirname 44 45 46def _ProcessSourceFile(output_dir, source_file_path, source_dirs): 47 source_file_path = os.path.normpath(os.path.join(output_dir, 48 source_file_path)) 49 java_root = _GetJavaRoot(source_file_path) 50 logging.debug('Extracted java root `%s` from source file path `%s`', 51 java_root, source_file_path) 52 source_dirs.add(java_root) 53 54 55def _ProcessSourcesFile(output_dir, sources_file_path, source_dirs): 56 for source_file_path in build_utils.ReadSourcesList( 57 os.path.join(output_dir, sources_file_path)): 58 _ProcessSourceFile(output_dir, source_file_path, source_dirs) 59 60 61def _ProcessBuildConfigFile(output_dir, build_config_path, source_dirs, libs, 62 already_processed_build_config_files, 63 android_sdk_build_tools_version): 64 if build_config_path in already_processed_build_config_files: 65 return 66 already_processed_build_config_files.add(build_config_path) 67 68 logging.info('Processing build config: %s', build_config_path) 69 70 with open(os.path.join(output_dir, build_config_path)) as build_config_file: 71 build_config = json.load(build_config_file) 72 73 deps_info = build_config['deps_info'] 74 target_sources_file = deps_info.get('target_sources_file') 75 if target_sources_file is not None: 76 _ProcessSourcesFile(output_dir, target_sources_file, source_dirs) 77 else: 78 unprocessed_jar_path = deps_info.get('unprocessed_jar_path') 79 if unprocessed_jar_path is not None: 80 lib_path = os.path.normpath(os.path.join(output_dir, 81 unprocessed_jar_path)) 82 logging.debug('Found lib `%s', lib_path) 83 libs.add(lib_path) 84 85 source_dirs.add( 86 os.path.join(output_dir, 87 _WithoutSuffix(build_config_path, '.build_config.json'), 88 'generated_java', 'input_srcjars')) 89 90 android = build_config.get('android') 91 if android is not None: 92 # This works around an issue where the language server complains about 93 # `java.lang.invoke.LambdaMetafactory` not being found. The normal Android 94 # build process is fine with this class being missing because d8 removes 95 # references to LambdaMetafactory from the bytecode - see: 96 # https://jakewharton.com/androids-java-8-support/#native-lambdas 97 # When JDT builds the code, d8 doesn't run, so the references are still 98 # there. Fortunately, the Android SDK provides a convenience JAR to fill 99 # that gap in: 100 # //third_party/android_sdk/public/build-tools/*/core-lambda-stubs.jar 101 libs.add( 102 os.path.normpath( 103 os.path.join( 104 output_dir, 105 os.path.dirname(build_config['android']['sdk_jars'][0]), 106 os.pardir, os.pardir, 'build-tools', 107 android_sdk_build_tools_version, 'core-lambda-stubs.jar'))) 108 109 for dep_config in deps_info['deps_configs']: 110 _ProcessBuildConfigFile(output_dir, dep_config, source_dirs, libs, 111 already_processed_build_config_files, 112 android_sdk_build_tools_version) 113 114 115def _GenerateClasspathEntry(kind, path): 116 classpathentry = xml.etree.ElementTree.Element('classpathentry') 117 classpathentry.set('kind', kind) 118 classpathentry.set('path', f'_/{path}') 119 return classpathentry 120 121 122def _GenerateClasspathFile(source_dirs, libs): 123 classpath = xml.etree.ElementTree.Element('classpath') 124 for source_dir in source_dirs: 125 classpath.append(_GenerateClasspathEntry('src', source_dir)) 126 for lib in libs: 127 classpath.append(_GenerateClasspathEntry('lib', lib)) 128 129 xml.etree.ElementTree.ElementTree(classpath).write(sys.stdout, 130 encoding='unicode') 131 132 133def _ParseArguments(argv): 134 parser = argparse.ArgumentParser( 135 description= 136 'Given Chromium Java build config files, dumps an Eclipse JDT classpath ' 137 'file to standard output that can be used with the "Language Support for ' 138 'Java™ by Red Hat" Visual Studio Code extension. See //docs/vscode.md ' 139 'for details.') 140 parser.add_argument( 141 '--output-dir', 142 required=True, 143 help='Relative path to the output directory, e.g. "out/Debug"') 144 parser.add_argument( 145 '--build-config', 146 action='append', 147 required=True, 148 help='Path to the .build_config.json file to use as input, relative to ' 149 '`--output-dir`. May be repeated.') 150 return parser.parse_args(argv) 151 152 153def main(argv): 154 build_utils.InitLogging('GENERATE_VSCODE_CLASSPATH_DEBUG') 155 args = _ParseArguments(argv) 156 output_dir = args.output_dir 157 158 build_vars = gn_helpers.ReadBuildVars(output_dir) 159 160 source_dirs = set() 161 libs = set() 162 already_processed_build_config_files = set() 163 for build_config_path in args.build_config: 164 _ProcessBuildConfigFile(output_dir, build_config_path, source_dirs, libs, 165 already_processed_build_config_files, 166 build_vars['android_sdk_build_tools_version']) 167 168 logging.info('Done processing %d build config files', 169 len(already_processed_build_config_files)) 170 171 _GenerateClasspathFile(source_dirs, libs) 172 173 174if __name__ == '__main__': 175 sys.exit(main(sys.argv[1:])) 176