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 an Eclipse JDT project that can 6be used 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 input_srcjars = os.path.join(output_dir, 86 _WithoutSuffix(build_config_path, '.build_config.json'), 87 'generated_java', 'input_srcjars') 88 if os.path.exists(input_srcjars): 89 source_dirs.add(input_srcjars) 90 91 android = build_config.get('android') 92 if android is not None: 93 # This works around an issue where the language server complains about 94 # `java.lang.invoke.LambdaMetafactory` not being found. The normal Android 95 # build process is fine with this class being missing because d8 removes 96 # references to LambdaMetafactory from the bytecode - see: 97 # https://jakewharton.com/androids-java-8-support/#native-lambdas 98 # When JDT builds the code, d8 doesn't run, so the references are still 99 # there. Fortunately, the Android SDK provides a convenience JAR to fill 100 # that gap in: 101 # //third_party/android_sdk/public/build-tools/*/core-lambda-stubs.jar 102 libs.add( 103 os.path.normpath( 104 os.path.join( 105 output_dir, 106 os.path.dirname(build_config['android']['sdk_jars'][0]), 107 os.pardir, os.pardir, 'build-tools', 108 android_sdk_build_tools_version, 'core-lambda-stubs.jar'))) 109 110 for dep_config in deps_info['deps_configs']: 111 _ProcessBuildConfigFile(output_dir, dep_config, source_dirs, libs, 112 already_processed_build_config_files, 113 android_sdk_build_tools_version) 114 115 116def _GenerateClasspathEntry(kind, path): 117 classpathentry = xml.etree.ElementTree.Element('classpathentry') 118 classpathentry.set('kind', kind) 119 classpathentry.set('path', path) 120 return classpathentry 121 122 123def _GenerateProject(source_dirs, libs, output_dir): 124 classpath = xml.etree.ElementTree.Element('classpath') 125 for source_dir in sorted(source_dirs): 126 classpath.append(_GenerateClasspathEntry('src', source_dir)) 127 for lib in sorted(libs): 128 classpath.append(_GenerateClasspathEntry('lib', lib)) 129 classpath.append( 130 _GenerateClasspathEntry('output', os.path.join(output_dir, 'jdt_output'))) 131 132 xml.etree.ElementTree.ElementTree(classpath).write( 133 '.classpath', encoding='unicode') 134 print('Generated .classpath', file=sys.stderr) 135 136 with open('.project', 'w') as f: 137 f.write("""<?xml version="1.0" encoding="UTF-8"?> 138<projectDescription> 139 <name>chromium</name> 140 <buildSpec> 141 <buildCommand> 142 <name>org.eclipse.jdt.core.javabuilder</name> 143 <arguments /> 144 </buildCommand> 145 </buildSpec> 146 <natures><nature>org.eclipse.jdt.core.javanature</nature></natures> 147</projectDescription> 148""") 149 print('Generated .project', file=sys.stderr) 150 151 # Tell the Eclipse compiler not to use java.lang.invoke.StringConcatFactory 152 # in the generated bytecodes as the class is unavailable in Android. 153 os.makedirs('.settings', exist_ok=True) 154 with open('.settings/org.eclipse.jdt.core.prefs', 'w') as f: 155 f.write("""eclipse.preferences.version=1 156org.eclipse.jdt.core.compiler.codegen.useStringConcatFactory=disabled 157""") 158 print('Generated .settings', file=sys.stderr) 159 160 161def _ParseArguments(argv): 162 parser = argparse.ArgumentParser( 163 description= 164 'Given Chromium Java build config files, generates an Eclipse JDT ' 165 'project that can be used with the "Language Support for Java™ by ' 166 'Red Hat" Visual Studio Code extension. See //docs/vscode.md ' 167 'for details.') 168 parser.add_argument( 169 '--output-dir', 170 required=True, 171 help='Relative path to the output directory, e.g. "out/Debug"') 172 parser.add_argument( 173 '--build-config', 174 action='append', 175 required=True, 176 help='Path to the .build_config.json file to use as input, relative to ' 177 '`--output-dir`. May be repeated.') 178 return parser.parse_args(argv) 179 180 181def main(argv): 182 build_utils.InitLogging('GENERATE_VSCODE_CLASSPATH_DEBUG') 183 184 assert os.path.exists('.gn'), 'This script must be run from the src directory' 185 186 args = _ParseArguments(argv) 187 output_dir = args.output_dir 188 189 build_vars = gn_helpers.ReadBuildVars(output_dir) 190 191 source_dirs = set() 192 libs = set() 193 already_processed_build_config_files = set() 194 for build_config_path in args.build_config: 195 _ProcessBuildConfigFile(output_dir, build_config_path, source_dirs, libs, 196 already_processed_build_config_files, 197 build_vars['android_sdk_build_tools_version']) 198 199 logging.info('Done processing %d build config files', 200 len(already_processed_build_config_files)) 201 202 _GenerateProject(source_dirs, libs, output_dir) 203 204 205if __name__ == '__main__': 206 sys.exit(main(sys.argv[1:])) 207