blob: 7e4f59d0675e9542cb23d23712af91c881d036a8 [file]
#!/usr/bin/env python3
#
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generates Java module classes from @ModuleInterface-annotated interfaces."""
import argparse
import os
import pathlib
import re
import sys
import zipfile
sys.path.append(
os.path.join(
os.path.dirname(__file__),
"..",
"..",
"..",
"build",
)
)
import action_helpers
import zip_helpers
_PACKAGE_RE = re.compile(r"^package\s+([\w.]+)\s*;", re.MULTILINE)
_MODULE_INTERFACE_RE = re.compile(
r"@ModuleInterface\((.*?)\)\s*(?:@\w+(?:\([^)]*\))?\s*)*"
r"(?:public\s+)?interface\s+(\w+)",
re.DOTALL,
)
_STRING_LITERAL_PAT = r'"[^"]*"(?:\s*\+\s*"[^"]*")*'
_MODULE_ARG_RE = re.compile(rf"\bmodule\s*=\s*({_STRING_LITERAL_PAT})")
_IMPL_ARG_RE = re.compile(rf"\bimpl\s*=\s*({_STRING_LITERAL_PAT})")
_TEMPLATE = """\
// This file is generated by:
// components/module_installer/android/module_interface_processor.py
// Please do not edit.
package {PACKAGE};
import android.annotation.SuppressLint;
import android.content.Context;
import org.chromium.build.annotations.IdentifierNameString;
import org.chromium.build.annotations.NullMarked;
import org.chromium.build.annotations.Nullable;
import org.chromium.components.module_installer.builder.Module;
import org.chromium.components.module_installer.engine.InstallEngine;
import org.chromium.components.module_installer.engine.InstallListener;
@NullMarked
public class {MODULE_CLASS_NAME} {{
public static final String SPLIT_NAME = "{MODULE_NAME}";
@IdentifierNameString
private static String sModuleClassString = "{IMPL_CLASS_NAME}";
@SuppressLint("StaticFieldLeak")
private static final Module<{INTERFACE_NAME}> sModule =
new Module<>(
SPLIT_NAME, {INTERFACE_NAME}.class, sModuleClassString);
private {MODULE_CLASS_NAME}() {{}}
public static boolean isInstalled() {{
return sModule.isInstalled();
}}
public static void install(InstallListener listener) {{
sModule.install(listener);
}}
public static void installDeferred() {{
sModule.installDeferred();
}}
public static void ensureNativeLoaded() {{
sModule.ensureNativeLoaded();
}}
public static {INTERFACE_NAME} getImpl() {{
return sModule.getImpl();
}}
public static InstallEngine getInstallEngine() {{
return sModule.getInstallEngine();
}}
public static void setInstallEngine(InstallEngine engine) {{
sModule.setInstallEngine(engine);
}}
public static @Nullable Context getContext() {{
return sModule.getContext();
}}
}}
"""
def _unquote_java_string(raw_expr):
return "".join(re.findall(r'"([^"]*)"', raw_expr))
def _process_file(source_path):
content = pathlib.Path(source_path).read_text(encoding="utf-8")
package_match = _PACKAGE_RE.search(content)
if not package_match:
sys.exit(f"Could not find package declaration in {source_path}")
package_name = package_match.group(1)
matches = list(_MODULE_INTERFACE_RE.finditer(content))
if not matches:
sys.exit(f"Could not find @ModuleInterface in {source_path}")
results = []
for match in matches:
args_str, interface_name = match.groups()
module_match = _MODULE_ARG_RE.search(args_str)
impl_match = _IMPL_ARG_RE.search(args_str)
if not module_match or not impl_match:
sys.exit(
"Could not parse module/impl attributes in @ModuleInterface in "
f"{source_path}"
)
module_name = _unquote_java_string(module_match.group(1))
impl_class_name = _unquote_java_string(impl_match.group(1))
module_class_name = (
"".join(w.capitalize() for w in module_name.split("_")) + "Module"
)
zip_path = f'{package_name.replace(".", "/")}/{module_class_name}.java'
java_code = _TEMPLATE.format(
PACKAGE=package_name,
MODULE_CLASS_NAME=module_class_name,
MODULE_NAME=module_name,
IMPL_CLASS_NAME=impl_class_name,
INTERFACE_NAME=interface_name,
)
results.append((zip_path, java_code))
return results
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--srcjar", required=True, help="Path to output srcjar."
)
parser.add_argument(
"sources", nargs="+", help="Input .java files with @ModuleInterface."
)
options = parser.parse_args()
with action_helpers.atomic_output(options.srcjar) as f:
with zipfile.ZipFile(f, "w") as srcjar_file:
for source_path in options.sources:
for zip_path, java_code in _process_file(source_path):
zip_helpers.add_to_zip_hermetic(
srcjar_file, zip_path, data=java_code
)
if __name__ == "__main__":
sys.exit(main())