0

Improve Visual Studio Code Java setup

This CL introduces a new way to set up VS Code to edit Java code in
Chromium. It makes it possible for the Java language server to
successfully build .java files in a way that is (mostly) identical to
the way the Chromium build system builds them, and without relying on a
manually curated Eclipse classpath file. This makes it possible to use
the full potential of the VS Code Java extension including full semantic
analysis, real-time reporting of build errors, etc.

Change-Id: I3a8e7260216a029936b9a684d64814da43dfb307
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4604730
Reviewed-by: Andrew Grieve <agrieve@chromium.org>
Commit-Queue: Etienne Dechamps <edechamps@google.com>
Auto-Submit: Etienne Dechamps <edechamps@google.com>
Cr-Commit-Position: refs/heads/main@{#1156225}
This commit is contained in:
Etienne Dechamps
2023-06-12 15:52:24 +00:00
committed by Chromium LUCI CQ
parent da61b62e98
commit 2d40c5554e
3 changed files with 228 additions and 55 deletions

@ -4,3 +4,5 @@ mheikal@chromium.org
pasko@chromium.org
smaier@chromium.org
wnwen@chromium.org
per-file generate_vscode_classpath.py=edechamps@google.com

@ -0,0 +1,175 @@
#!/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:]))

@ -226,65 +226,61 @@ Note: See also [Key Bindings for Visual Studio Code
### Java/Android Support
*Before anything*, add these to your settings.json.
```
// LightWeight is the language support, the feature we care about. The other
// modes include build functionality with Maven and Gradle. They try to build
// on their own and end up showing thousands of errors.
"java.server.launchMode": "LightWeight",
// Avoids overwriting the custom .classpath file (c.f. next section).
"java.configuration.updateBuildConfiguration": "disabled",
```
Then install the "Language Support for Java" extension. If you installed it
before setting the configs above, uninstall, delete the <project> folder (c.f.
next section) and reinstall. You also don't need any of the remaining extensions
in "Extension Pack for Java".
Follow these steps to get full IDE support (outline, autocompletion, jump to
definition including automatic decompilation of prebuilts, real-time reporting
of compile errors/warnings, Javadocs, etc.) when editing `.java` files in
Chromium:
#### Setting up code completion/reference finding/etc.
1. **Add the following to your VS Code workspace `settings.json`:**
* `"java.import.gradle.enabled": false`
`"java.import.maven.enabled": false`
This will prevent the language server from attempting to build *all*
Gradle and Maven projects that can be found anywhere in the Chromium
source tree, which typically results in hilarity.
* `"java.jdt.ls.java.home": "<< ABSOLUTE PATH TO YOUR WORKING COPY OF CHROMIUM >>/src/third_party/jdk/current"`
This one is optional but reduces the likelihood of problems by making sure
the language server uses the same JDK as the Chromium build system (as
opposed to some random JDK from your host system).
2. **Install the
[*Language Support for Java™ by Red Hat*](https://marketplace.visualstudio.com/items?itemName=redhat.java)
extension.**
You do not need any other extension.
3. In the VS Code explorer (left-hand pane) **right-click on a folder (any folder)
and click *Add Folder to Java Source Path***.
This is just one way to force the extension to generate an internal build
project for the language server. The specific folder doesn't matter because
the source path configuration will be overwritten in step 6.
4. **Wait for the *Successfully added '...' to the project src_...'s source
path* pop-up notification to appear**.
Note that this can take some time (1-2 minutes), during which VS Code seems
to be idle.
5. **Build your code** in the usual way (i.e. using gn and ninja commands).
This will produce build config files that are necessary for the next step. It
will also make autogenerated code visible to the language server.
6. **Generate the `.classpath` file** for the internal build project by running
`build/android/generate_vscode_classpath.py` with the appropriate arguments
from the root of your VS Code workspace folder.
For example, if your VS Code workspace is rooted in `src`, your build
output directory is `out/Debug-x86` and your build target is
`//components/cronet/android:cronet_javatests`, run:
`build/android/generate_vscode_classpath.py --output-dir out/Debug-x86 --build-config gen/components/cronet/android/cronet_javatests.build_config.json > ~/.vscode*/data/User/workspaceStorage/*/redhat.java/jdt_ws/.metadata/.plugins/org.eclipse.core.resources/.projects/src_*/.classpath`
7. **Reload** your VS Code window.
8. **Open a Java source file then wait a couple of minutes** for the language
server to build the project.
9. **Done!** You should now have full Java language support for any `.java` file
that is included in the build.
You'll need to generate a placeholder .classpath file and locate it. In order
to generate it, right click on any Java source folder in the left panel and
choose "Add folder to java source path". Its location will depend on whether
you're doing local or remote development. Local path on linux will look
something like:
*** note
**Warning:** do not attempt to change the extension's source path settings, as
that risks overwriting the generated `.classpath` file.
***
`~/.vscode/data/User/workspaceStorage/<hash>/redhat.java/jdt_ws/<project>/.classpath`
#### Known issues
You might find multiple folders when looking for `<project>`. Choose anything except
`jdt.ls-java-project`. If you only see `jdt.ls-java-project`, try using the
"Add folder to java source path" option again.
If doing remote development, the file will be under `~/.vscode-server/` on your
remote machine.
You'll need to replace all of the contents of that file with the contents of
`tools/android/eclipse/.classpath` (external) or
`clank/development/ide/eclipse/.classpath` (generated by gclient runhooks for
Chrome developers), and then replace some paths as vscode interprets some paths
differently from eclipse.
* Replace: `kind="src" path="` with `kind="src" path="_/`
* eg. `<classpathentry kind="src" path="_/android_webview/glue/java/src"/>`
* Replace: `kind="lib" path="../src` with `kind="lib" path="_`
* eg.
`<classpathentry kind="lib" path="_/out/Debug/lib.java/base/base_java.jar"/>`
* Remove all nested paths (or exclude them from their parents). At time of
writing:
* `third_party/android_protobuf/src/java/src/main/java`
* `third_party/junit/src/src/main/java`
Also, make sure
`export ANDROID_HOME=/usr/local/google/home/{your_ldap}/Android/Sdk` is in the
remote machine's `~/.bashrc`.
Then restart vscode, open a Java file, and wait for a bit.
Debugging tips:
* Right clicking on a folder in vscode and clicking "Add folder to java source
path" will error if there are syntax problems with your classpath. (Don't use
this actually add new paths to your classpath as it won't work correctly)
* If there are no syntax errors, ensure the correct .classpath file is being
used by seeing if the folder was actually added to the .classpath file you
edited.
* Errors related to `GEN_JNI` are caused by the language server (rightfully)
getting confused about multiple definitions of the
[autogenerated](/base/android/jni_generator/README.md) `GEN_JNI` class. This
is a known quirk of the JNI generator.
## Setup For Chromium