| #!/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()) |