blob: 568ece4dcb5ed6bcb391665a43f21df28aa10c73 [file]
#!/usr/bin/env python3
# Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
import argparse
from concurrent.futures import ThreadPoolExecutor
import os
from subprocess import CalledProcessError, DEVNULL, PIPE, Popen, STDOUT, check_call, check_output
import sys
# Support running both directly as a script (where tools/ is in sys.path)
# and when imported as a module from the repository root (e.g. PRESUBMIT.py).
try:
from tools import defines, google_java_format_diff, jdk, utils
except ImportError:
import defines
import google_java_format_diff
import jdk
import utils
GOOGLE_JAVA_FORMAT_DIR = os.path.join(defines.THIRD_PARTY, 'google',
'google-java-format', '1.24.0')
GOOGLE_JAVA_FORMAT_JAR = os.path.join(GOOGLE_JAVA_FORMAT_DIR,
'google-java-format-1.24.0-all-deps.jar')
GOOGLE_KOTLIN_FORMAT_DIR = os.path.join(defines.THIRD_PARTY, 'google',
'google-kotlin-format', '0.54')
GOOGLE_KOTLIN_FORMAT_JAR = os.path.join(GOOGLE_KOTLIN_FORMAT_DIR,
'ktfmt-0.54-jar-with-dependencies.jar')
FMT_IGNORE = {
'src/test/java/com/android/tools/r8/kotlin/metadata/inline_class_fun_descriptor_classes_app/main.kt'
}
GOOGLE_YAPF_DIR = os.path.join(defines.THIRD_PARTY, 'google', 'yapf',
'20231013')
GOOGLE_YAPF = os.path.join(GOOGLE_YAPF_DIR, 'yapf')
YAPF_PYTHON_PATH = [
GOOGLE_YAPF_DIR,
os.path.join(GOOGLE_YAPF_DIR, 'third_party')
]
NODE_DIR = os.path.join(defines.THIRD_PARTY, 'node', '24.16.0', 'linux')
NODE_BIN = os.path.join(NODE_DIR, 'bin', 'node')
PRETTIER_DIR = os.path.join(defines.THIRD_PARTY, 'prettier', '3.8.3')
PRETTIER_BIN = os.path.join(PRETTIER_DIR, 'node_modules', 'prettier', 'bin',
'prettier.cjs')
PLATFORMDIRS_ERROR_MSG = (
"Error: Could not find a Python interpreter with `platformdirs` installed.\n"
"Please ensure it is installed in your environment:\n"
" $ python3 -m venv .venv\n"
" $ source .venv/bin/activate\n"
" $ pip3 install platformdirs")
def is_java_file(file_path):
return file_path.endswith('.java')
def is_kotlin_file(file_path):
return file_path.endswith('.kt') or file_path.endswith('.kts')
def is_python_file(file_path):
return file_path.endswith('.py')
def is_web_file(file_path):
return file_path.endswith(('.js', '.html', '.css'))
def is_formattable_file(file_path):
return (is_java_file(file_path) or is_kotlin_file(file_path) or
is_python_file(file_path) or is_web_file(file_path))
def get_changed_files(upstream, file_predicate=None):
changed_files = check_output(['git', 'diff', '--name-only',
upstream]).decode('utf-8').splitlines()
return [
f for f in changed_files if f not in FMT_IGNORE and
(file_predicate is None or file_predicate(f)) and os.path.exists(f)
]
def get_dir_files(directory, file_predicate=None):
predicate = file_predicate or is_formattable_file
if not os.path.exists(directory):
print(f"Error: Directory or file not found: {directory}",
file=sys.stderr)
return []
if os.path.isfile(directory):
if directory not in FMT_IGNORE and predicate(directory):
return [directory]
return []
files = []
for root, _, filenames in os.walk(directory):
for filename in filenames:
file_path = os.path.join(root, filename)
if file_path not in FMT_IGNORE and predicate(file_path):
files.append(file_path)
return files
def chunk_list(items, chunk_size):
for i in range(0, len(items), chunk_size):
yield items[i:i + chunk_size]
def partition_kotlin_files(files):
return {
'--kotlinlang-style': [
p for p in files if p.replace('\\', '/').startswith('src/keepanno/')
],
'--google-style': [
p for p in files
if not p.replace('\\', '/').startswith('src/keepanno/')
],
}
def get_env_with_python_path():
new_env = os.environ.copy()
new_env['PYTHONPATH'] = os.pathsep.join(YAPF_PYTHON_PATH)
return new_env
class PythonRuntime:
def __init__(self):
self.interpreter = None
self.has_failed = False
def initialize_runtime(self):
python_env = get_env_with_python_path()
for candidate in [
sys.executable, 'python3',
os.path.join('.venv', 'bin', 'python3'),
os.path.join('.venv', 'bin', 'python')
]:
try:
check_call([candidate, '-c', 'import platformdirs'],
stdout=DEVNULL,
stderr=DEVNULL,
env=python_env)
self.interpreter = candidate
return None
except (CalledProcessError, FileNotFoundError):
continue
self.has_failed = True
return PLATFORMDIRS_ERROR_MSG
def get_java_format_base_command():
java_bin = jdk.GetJavaExecutable(jdk.GetDefaultJdkHome())
return google_java_format_diff.get_base_command(java_bin,
GOOGLE_JAVA_FORMAT_JAR)
def get_kotlin_format_base_command():
java_exec = jdk.GetJavaExecutable(jdk.GetDefaultJdkHome())
return [java_exec, '-jar', GOOGLE_KOTLIN_FORMAT_JAR]
def get_python_format_base_command(python_runtime):
return [python_runtime.interpreter, GOOGLE_YAPF, '--style', 'google']
def get_prettier_base_command():
return [NODE_BIN, PRETTIER_BIN]
def get_git_upstream():
return check_output(['git', 'cl', 'upstream']).decode('utf-8').strip()
def prepare_check_java_files():
utils.ensure_google_download(GOOGLE_JAVA_FORMAT_DIR)
# This function must be threadsafe.
# prepare_check_java_files() must be called first.
# upstream_for_diff_formatting: If set, only changed lines are checked/formatted (best effort).
def check_java_files_precise_diff(files, upstream, fix_formatting=False):
if not upstream:
return [
"Error: Upstream branch/commit must be specified for per-line Java formatting."
]
if not files:
return []
try:
git_diff_cmd = ['git', 'diff', '-U0', '--no-color', upstream, '--'
] + files
diff_text = check_output(git_diff_cmd).decode('utf-8')
except CalledProcessError as e:
return [
f"Git diff error:\n{e.output.decode('utf-8') if e.output else str(e)}"
]
if not diff_text.strip():
return []
java_bin = jdk.GetJavaExecutable(jdk.GetDefaultJdkHome())
return google_java_format_diff.format_diff(diff_text,
GOOGLE_JAVA_FORMAT_JAR,
java_bin,
fix_formatting=fix_formatting,
prefix_strip=1)
# This function must be threadsafe.
# prepare_check_java_files() must be called first.
def check_java_files_full(files, fix_formatting=False):
if not files:
return []
cmd = get_java_format_base_command()
if fix_formatting:
cmd.extend(['-i'] + files)
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=DEVNULL)
_, stderr = proc.communicate()
if proc.returncode != 0:
err_msg = stderr.decode('utf-8') if stderr else "Unknown error"
return [f"Java formatting error:\n{err_msg}"]
return []
else:
cmd.extend(['--dry-run', '--set-exit-if-changed'] + files)
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=DEVNULL)
stdout, stderr = proc.communicate()
errors = []
if proc.returncode != 0:
if stdout:
base_command = get_java_format_base_command()
for file_path in stdout.decode('utf-8').splitlines():
errors.extend(
google_java_format_diff.format_file(
file_path, base_command, fix_formatting=False))
if stderr:
errors.append(
f"Java formatting error:\n{stderr.decode('utf-8')}")
elif not errors and not stdout:
errors.append("Java formatting error:\nUnknown error")
return errors
# This function must be threadsafe.
# prepare_check_java_files() must be called first.
# upstream_for_diff_formatting: If set, only changed lines are checked/formatted (best effort).
def check_java_files(files,
fix_formatting=False,
upstream_for_diff_formatting=None):
if upstream_for_diff_formatting is not None:
return check_java_files_precise_diff(files,
upstream_for_diff_formatting,
fix_formatting=fix_formatting)
else:
return check_java_files_full(files, fix_formatting=fix_formatting)
def prepare_check_kotlin_files():
utils.ensure_google_download(GOOGLE_KOTLIN_FORMAT_DIR)
# This function must be threadsafe.
# prepare_check_kotlin_files() must be called first.
def check_kotlin_files(files, format_style, fix_formatting=False):
cmd = get_kotlin_format_base_command() + [format_style]
if not fix_formatting:
cmd.append('-n')
cmd.extend(files)
if fix_formatting:
check_call(cmd)
return []
else:
result = check_output(cmd)
errors = []
if len(result) > 0:
for file_path in result.splitlines():
errors.append(
f"File {file_path.decode('utf-8')} needs formatting")
return errors
def prepare_check_python_files(python_runtime: PythonRuntime = None):
utils.ensure_google_download(GOOGLE_YAPF_DIR)
if python_runtime:
return python_runtime.initialize_runtime()
return None
# This function must be threadsafe.
# prepare_check_python_files() must be called first.
def check_python_files(files,
python_runtime: PythonRuntime,
fix_formatting=False):
mode_flag = '--in-place' if fix_formatting else '--diff'
format_cmd = get_python_format_base_command(python_runtime) + [mode_flag
] + files
python_env = get_env_with_python_path()
try:
check_output(format_cmd, stderr=STDOUT, env=python_env)
return []
except CalledProcessError as e:
output_str = (e.output.decode('utf-8')
if isinstance(e.output, bytes) else str(e.output))
if fix_formatting:
return [f"Python formatting error:\n{output_str}"]
return [output_str]
def prepare_check_web_files():
utils.ensure_google_download(NODE_DIR)
utils.ensure_google_download(PRETTIER_DIR)
# This function must be threadsafe.
# prepare_check_web_files() must be called first.
def check_web_files(files, fix_formatting=False):
mode_flag = '--write' if fix_formatting else '--check'
format_cmd = get_prettier_base_command() + [mode_flag] + files
if fix_formatting:
check_call(format_cmd)
return []
else:
try:
check_output(format_cmd, stderr=STDOUT)
return []
except CalledProcessError as e:
output_str = (e.output.decode('utf-8') if isinstance(
e.output, bytes) else str(e.output))
return [f"Web formatting error:\n{output_str}"]
# upstream_for_diff_formatting: If set, only changed lines are checked/formatted (best effort).
# Diff formatting is very slow (~1 second per file), so keep batch size small.
def check(files,
fix_formatting=False,
batch_size=5,
upstream_for_diff_formatting=None):
java_files = []
kotlin_files = []
python_files = []
web_files = []
for file_path in files:
if file_path in FMT_IGNORE or not os.path.exists(file_path):
pass
elif is_kotlin_file(file_path):
kotlin_files.append(file_path)
elif is_java_file(file_path):
java_files.append(file_path)
elif is_python_file(file_path):
python_files.append(file_path)
elif is_web_file(file_path):
web_files.append(file_path)
results = {
'Java': [],
'Kotlin': [],
'Python': [],
'Web (JS, HTML, CSS)': [],
}
if java_files:
prepare_check_java_files()
if kotlin_files:
prepare_check_kotlin_files()
python_runtime = PythonRuntime()
if python_files:
init_error = prepare_check_python_files(python_runtime)
if init_error:
results['Python'].append(init_error)
return results
if web_files:
prepare_check_web_files()
futures = []
with ThreadPoolExecutor() as executor:
for batch in chunk_list(java_files, batch_size):
futures.append((
'Java',
executor.submit(
check_java_files,
batch,
fix_formatting=fix_formatting,
upstream_for_diff_formatting=upstream_for_diff_formatting)))
for format_style, style_paths in partition_kotlin_files(
kotlin_files).items():
for batch in chunk_list(style_paths, batch_size):
futures.append(('Kotlin',
executor.submit(check_kotlin_files,
batch,
format_style,
fix_formatting=fix_formatting)))
for batch in chunk_list(python_files, batch_size):
futures.append(('Python',
executor.submit(check_python_files,
batch,
python_runtime,
fix_formatting=fix_formatting)))
for batch in chunk_list(web_files, batch_size):
futures.append(('Web (JS, HTML, CSS)',
executor.submit(check_web_files,
batch,
fix_formatting=fix_formatting)))
for lang, future in futures:
errors = future.result()
if errors:
results[lang].extend(errors)
return results
def parse_options():
result = argparse.ArgumentParser()
result.add_argument('--format-dir',
help='Format all files in the given directory.',
default=None)
result.add_argument(
'--format-precise-diff',
help=
'Only format changed lines (Java only, other formats are still formatted per file).',
action='store_true',
default=False)
result.add_argument(
'--upstream',
help=
'Upstream branch/commit to diff against for --format-precise-diff (default: git cl upstream).',
default=None)
result.add_argument('--only-java',
help='Only run google-java-format.',
action='store_true',
default=False)
result.add_argument('--only-kotlin',
help='Only run google-kotlin-format.',
action='store_true',
default=False)
result.add_argument('--only-python',
help='Only run YAPF.',
action='store_true',
default=False)
result.add_argument('--only-web',
help='Only run Prettier.',
action='store_true',
default=False)
return result.parse_known_args()
def format_files(files, upstream_for_diff_formatting=None):
results = check(files,
fix_formatting=True,
upstream_for_diff_formatting=upstream_for_diff_formatting)
for errors in results.values():
for err in errors:
print(err, file=sys.stderr)
def get_predicate(options):
if options.only_java:
return is_java_file
elif options.only_kotlin:
return is_kotlin_file
elif options.only_python:
return is_python_file
elif options.only_web:
return is_web_file
return None
def format_all_changed_files(upstream, options):
predicate = get_predicate(options)
upstream_for_diff_formatting = upstream if options.format_precise_diff else None
format_files(get_changed_files(upstream, predicate),
upstream_for_diff_formatting=upstream_for_diff_formatting)
def format_dir(directory, options):
predicate = get_predicate(options)
upstream_for_diff_formatting = options.upstream if options.format_precise_diff else None
format_files(get_dir_files(directory, predicate),
upstream_for_diff_formatting=upstream_for_diff_formatting)
def main():
(options, args) = parse_options()
only_options_count = sum([
options.only_java, options.only_kotlin, options.only_python,
options.only_web
])
if only_options_count > 1:
print("Error: Cannot specify multiple --only options.", file=sys.stderr)
return 1
if options.format_dir and options.format_precise_diff:
print("Error: Cannot both --format-dir and --format-precise-diff",
file=sys.stderr)
return 1
if options.format_dir:
format_dir(options.format_dir, options)
else:
upstream = options.upstream or get_git_upstream()
format_all_changed_files(upstream, options)
return 0
if __name__ == '__main__':
sys.exit(main())