blob: b2d935109ef1b60ed771b13fb3c50c260a16c09f [file]
#!/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 an Eclipse JDT project 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 re
import shutil
import sys
import xml.etree.ElementTree
import zipfile
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 _GetJavaReleaseVersion():
"""Read the --release value from compile_java.py."""
compile_java_path = os.path.join(os.path.dirname(__file__), 'gyp',
'compile_java.py')
with open(compile_java_path) as f:
contents = f.read()
m = re.search(r"'--release',\s*'(\d+)'", contents)
if m:
return m.group(1)
return '17'
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)
# Refuse output_dir/ and output_dir/gen/ as source roots — JDT would scan
# every sibling and the .classpath would be invalid.
output_dir_norm = os.path.normpath(output_dir)
if java_root in (output_dir_norm,
os.path.normpath(os.path.join(output_dir_norm, 'gen'))):
logging.warning('Skipping overly broad Java root `%s` for: %s',
java_root, source_file_path)
return
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 _FindCanonicalCompileResources(output_dir, build_config_path):
"""The __compile_resources.srcjar a root target owns, or None.
Used to pick a single coherent R for the project; see _IsUsefulSrcJar.
"""
params_json = build_config_path.replace('.build_config.json', '.params.json')
with open(os.path.join(output_dir, params_json)) as f:
params = json.load(f)
for s in params.get('bundled_srcjars', []):
if s.endswith('__compile_resources.srcjar'):
return s
return None
def _IsUsefulSrcJar(srcjar_path, canonical_compile_resources):
"""Drop bundled srcjars that would surface duplicate types in JDT.
The wrapper-srcjar filters (BuildConfig/NativeLibraries/ProductConfig) hide
per-apk variants whose classes are already fed via input_srcjars. Resource
srcjars are pinned to the root target's own to avoid R conflicts across
libs. Mirrors build/android/chromiumide_api.py:_is_useful_source_jar.
"""
if srcjar_path.endswith('_placeholder.srcjar'):
return False
if srcjar_path.endswith(('__build_config_srcjar.srcjar',
'__native_libraries.srcjar',
'__product_config_srcjar.srcjar')):
return False
if srcjar_path.endswith('__compile_resources.srcjar'):
return srcjar_path in canonical_compile_resources
return True
def _ExtractBundledSrcJar(output_dir, srcjar_path, source_dirs):
"""Extract a bundled .srcjar to a cache dir so JDT can index its sources.
Returns True if a source root was added (False for empty extracts).
Mirrors build/android/chromiumide_api.py:_extract_source_jar.
"""
abs_srcjar = os.path.normpath(os.path.join(output_dir, srcjar_path))
if not os.path.exists(abs_srcjar):
return False
extract_dir = os.path.normpath(
os.path.join(output_dir, 'extracted_srcjars',
_WithoutSuffix(srcjar_path, '.srcjar')))
# Sibling stamp = "extract finished cleanly". Missing/stale stamp triggers
# re-extract, so a mid-extract crash recovers on the next run.
stamp = extract_dir + '.stamp'
if (not os.path.exists(stamp)
or os.path.getmtime(stamp) < os.path.getmtime(abs_srcjar)):
if os.path.exists(extract_dir):
shutil.rmtree(extract_dir)
os.makedirs(extract_dir)
with zipfile.ZipFile(abs_srcjar) as zf:
zf.extractall(extract_dir)
# Strip placeholder GEN_JNI; every *_jni.srcjar ships one and they collide.
jni_zero_dir = os.path.join(extract_dir, 'org', 'jni_zero')
if os.path.exists(jni_zero_dir):
shutil.rmtree(jni_zero_dir)
with open(stamp, 'wb'):
pass
has_java = any(f.endswith('.java')
for _, _, files in os.walk(extract_dir) for f in files)
if has_java:
source_dirs.add(extract_dir)
return has_java
def _AddTargetOutput(output_dir, params_json, build_config, has_sources_file,
canonical_compile_resources, source_dirs, libs):
"""Resolve a target's generated/compiled output into source roots and libs.
Source priority (first match wins; alternatives would dup-type in JDT):
input_srcjars/ → bundled_srcjars (extracted). The compiled jar is added as
a classpath lib only for aggregating roots (apk/bundle/robolectric — they
carry final R/GEN_JNI/BuildConfig classes per
build/android/docs/java_toolchain.md) or true prebuilts.
"""
input_srcjars = os.path.join(output_dir,
_WithoutSuffix(params_json, '.params.json'),
'generated_java', 'input_srcjars')
has_source_repr = False
if os.path.exists(input_srcjars):
source_dirs.add(input_srcjars)
has_source_repr = True
else:
# Require srcjars to exist on disk so a stale/partial build doesn't
# suppress the prebuilt fallback below.
useful_bundled_srcjars = [
s for s in build_config.get('bundled_srcjars', [])
if s.startswith('gen/')
and _IsUsefulSrcJar(s, canonical_compile_resources)
and os.path.exists(os.path.join(output_dir, s))
]
for srcjar in useful_bundled_srcjars:
if _ExtractBundledSrcJar(output_dir, srcjar, source_dirs):
has_source_repr = True
unprocessed_jar_path = build_config.get('unprocessed_jar_path')
if unprocessed_jar_path is None:
return
is_aggregating = build_config.get('type') in ('android_apk',
'android_app_bundle_module',
'robolectric_binary')
if is_aggregating or (not has_source_repr and not has_sources_file):
lib_path = os.path.normpath(os.path.join(output_dir, unprocessed_jar_path))
logging.debug('Found lib `%s`', lib_path)
libs.add(lib_path)
def _ProcessBuildConfigFile(output_dir, build_config_path, source_dirs, libs,
already_processed_build_config_files,
android_sdk_build_tools_version,
canonical_compile_resources):
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)
# deps_configs (needed to walk the dependency tree) lives in
# .params.json rather than .build_config.json. Read both and merge.
params_json = build_config_path.replace('.build_config.json', '.params.json')
with open(os.path.join(output_dir, params_json)) as f:
build_config = json.load(f)
build_config_file = os.path.join(output_dir, build_config_path)
if os.path.exists(build_config_file):
with open(build_config_file) as f:
build_config.update(json.load(f))
target_sources_file = build_config.get('target_sources_file')
if target_sources_file is not None:
_ProcessSourcesFile(output_dir, target_sources_file, source_dirs)
# Add pre-built JARs that are compile-time dependencies (e.g., SDK JARs).
for jar_path in build_config.get('input_jars_paths', []):
lib_path = os.path.normpath(os.path.join(output_dir, jar_path))
if os.path.exists(lib_path):
logging.debug('Found input jar `%s`', lib_path)
libs.add(lib_path)
_AddTargetOutput(output_dir, params_json, build_config,
target_sources_file is not None,
canonical_compile_resources, source_dirs, libs)
# Add core-lambda-stubs.jar so that JDT can resolve LambdaMetafactory.
# d8 normally strips these refs, but JDT doesn't run d8. See:
# https://jakewharton.com/androids-java-8-support/#native-lambdas
lambda_stubs_jar = os.path.join('third_party', 'android_sdk', 'public',
'build-tools',
android_sdk_build_tools_version,
'core-lambda-stubs.jar')
if os.path.exists(lambda_stubs_jar):
libs.add(os.path.normpath(lambda_stubs_jar))
for dep_config in build_config.get('deps_configs', []):
_ProcessBuildConfigFile(output_dir, dep_config, source_dirs, libs,
already_processed_build_config_files,
android_sdk_build_tools_version,
canonical_compile_resources)
# Also walk public_deps_configs to pick up transitive dependencies
# exposed via public_deps (e.g., chrome_public_apk's full dep tree).
for dep_config in build_config.get('public_deps_configs', []):
_ProcessBuildConfigFile(output_dir, dep_config, source_dirs, libs,
already_processed_build_config_files,
android_sdk_build_tools_version,
canonical_compile_resources)
def _GenerateClasspathEntry(kind, path, excluding=None):
classpathentry = xml.etree.ElementTree.Element('classpathentry')
classpathentry.set('kind', kind)
classpathentry.set('path', path)
if excluding:
classpathentry.set('excluding', excluding)
return classpathentry
def _GenerateProject(source_dirs, libs, output_dir):
# Find source dirs that are ancestors of other source dirs. Eclipse JDT
# does not allow nesting, but supports exclusion patterns to carve out
# subdirectories that have their own source entries.
sorted_dirs = sorted(source_dirs)
# Map parent -> list of nested children (relative to parent)
nested_children = {}
for i, d in enumerate(sorted_dirs):
for other in sorted_dirs[i + 1:]:
if other.startswith(d + '/'):
nested_children.setdefault(d, []).append(other[len(d) + 1:] + '/')
elif not other.startswith(d):
break
classpath = xml.etree.ElementTree.Element('classpath')
for source_dir in sorted(source_dirs):
children = nested_children.get(source_dir)
if children:
classpath.append(
_GenerateClasspathEntry('src',
source_dir,
excluding='|'.join(children)))
else:
classpath.append(_GenerateClasspathEntry('src', source_dir))
for lib in sorted(libs):
classpath.append(_GenerateClasspathEntry('lib', lib))
classpath.append(
_GenerateClasspathEntry('output', os.path.join(output_dir, 'jdt_output')))
xml.etree.ElementTree.ElementTree(classpath).write(
'.classpath', encoding='unicode')
print('Generated .classpath', file=sys.stderr)
with open('.project', 'w') as f:
f.write("""<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>chromium</name>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments />
</buildCommand>
</buildSpec>
<natures><nature>org.eclipse.jdt.core.javanature</nature></natures>
</projectDescription>
""")
print('Generated .project', file=sys.stderr)
# Tell the Eclipse compiler not to use java.lang.invoke.StringConcatFactory
# in the generated bytecodes as the class is unavailable in Android.
# Set compiler compliance to match Chromium's javac --release value
# (read from compile_java.py).
java_release = _GetJavaReleaseVersion()
os.makedirs('.settings', exist_ok=True)
with open('.settings/org.eclipse.jdt.core.prefs', 'w') as f:
f.write(f"""eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.useStringConcatFactory=disabled
org.eclipse.jdt.core.compiler.source={java_release}
org.eclipse.jdt.core.compiler.compliance={java_release}
org.eclipse.jdt.core.compiler.codegen.targetPlatform={java_release}
""")
print('Generated .settings', file=sys.stderr)
def _ParseArguments(argv):
parser = argparse.ArgumentParser(
description=
'Given Chromium Java build config files, generates an Eclipse JDT '
'project 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')
assert os.path.exists('.gn'), 'This script must be run from the src directory'
args = _ParseArguments(argv)
output_dir = args.output_dir
build_vars = gn_helpers.ReadBuildVars(output_dir)
# Pin R aggregation to root targets; other __compile_resources.srcjars in
# the dep tree are partial duplicates that surface R conflicts in JDT.
canonical_compile_resources = set()
for build_config_path in args.build_config:
found = _FindCanonicalCompileResources(output_dir, build_config_path)
if found is not None:
canonical_compile_resources.add(found)
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'],
canonical_compile_resources)
logging.info('Done processing %d build config files',
len(already_processed_build_config_files))
_GenerateProject(source_dirs, libs, output_dir)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))