Skip to main content
Glama
create_jdk_system_image.py9.15 kB
#!/usr/bin/python3 # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under both the MIT license found in the # LICENSE-MIT file in the root directory of this source tree and the Apache # License, Version 2.0 found in the LICENSE-APACHE file in the root directory # of this source tree. """ Converts core-for-system-modules.jar from Android SDK release to a Java system image given to the --system flag for when compiling for Java target 9+. This entire script is translated from Gradle: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/JdkImageTransformDelegate.kt See also the equivalent feature in Bazel Android build rules: https://github.com/bazelbuild/rules_android/commit/20c8e5bef957a2334346490934bf3dc54942457c New comments are marked with "META:" """ from __future__ import annotations import argparse import os import re import shutil import subprocess import sys import tempfile import zipfile from pathlib import Path from typing import List # META: Translation of ProcessBuilder. Note some tools print error to stdout so print both # stdout+stderr on failure. def run_subprocess(args: List[str]) -> bytes: try: return subprocess.check_output(args, stderr=subprocess.PIPE) except subprocess.CalledProcessError as e: print(f"Failed to run {args}: exit code={e.returncode}", file=sys.stderr) if e.stdout: print("stdout:", file=sys.stderr) print(e.stdout.decode("utf-8"), file=sys.stderr) if e.stderr: print("stderr:", file=sys.stderr) print(e.stderr.decode("utf-8"), file=sys.stderr) sys.exit(1) class JdkImageTransformDelegate: def __init__( self, system_modules_jar: Path, work_dir: Path, out_dir: Path, jdk_tools: JdkTools, ): self.system_modules_jar = system_modules_jar self.work_dir = work_dir self.out_dir = out_dir self.jdk_tools = jdk_tools def run(self): """ Links a JDK Image from [system_modules_jar] in [out_dir], using [work_dir] for the output of any intermediate operations. This is done via the "jlink" tool as exposed by [jdk_tools]. """ # jlink takes a directory of JMOD files as inputs jmod_file = self.make_jmod_file() jmod_dir = jmod_file.parent self.jdk_tools.link_jmods_into_jdk_image(jmod_dir, "java.base", self.out_dir) copy_jrt_fs_jar(self.out_dir, self.jdk_tools) def make_module_descriptor_java(self) -> Path: """ Creates a "module-info.java" file describing the contents of system_modules_jar """ module_info_java_content = generate_module_descriptor( "java.base", [self.system_modules_jar] ) module_info_java = self.work_dir / "module-info.java" with open(module_info_java, "w") as f: f.write(module_info_java_content) return module_info_java def make_module_info_class(self) -> Path: """ Creates and compiles a "module-info.class" file describing the contents of system_modules_jar """ module_info_java = self.make_module_descriptor_java() self.jdk_tools.compile_module_descriptor( module_info_java, self.system_modules_jar, self.work_dir ) module_info_class = self.work_dir / "module-info.class" assert module_info_class.exists(), f"Expected compiled module descriptor file to be created at {module_info_class}" return module_info_class def make_module_jar(self, work_dir: Path) -> Path: """ Creates a Modular Jar from [system_modules_jar] by compiling and adding a module descriptor """ module_info_class = self.make_module_info_class() module_jar = work_dir / "module.jar" create_jar( self.jdk_tools.java, self.jdk_tools.jar_builder, module_info_class, [self.system_modules_jar], module_jar, ) return module_jar def make_jmod_file(self) -> Path: module_name = "java.base" jlink_version = self.jdk_tools.jlink_version() jmod_dir = self.work_dir / "jmod" os.mkdir(jmod_dir) jmod_file = jmod_dir / f"{module_name}.jmod" module_jar = self.make_module_jar(self.work_dir) self.jdk_tools.create_jmod_from_modular_jar( jmod_file, jlink_version, module_jar ) return jmod_file class JdkTools: def __init__( self, java: Path, javac: Path, jlink: Path, jmod: Path, jrt_fs_jar: Path, jar_builder: Path, ): self.java = java self.javac = javac self.jlink = jlink self.jmod = jmod self.jrt_fs_location = jrt_fs_jar self.jar_builder = jar_builder def jlink_version(self): return run_subprocess([self.jlink, "--version"]).decode("utf-8").strip() def compile_module_descriptor( self, module_info_java: Path, system_modules_jar: Path, out_dir: Path ): classpath_arg_value = str(system_modules_jar) run_subprocess( [ self.javac, "--system=none", f"--patch-module=java.base={classpath_arg_value}", "-d", out_dir, module_info_java, ] ) def create_jmod_from_modular_jar( self, jmod_file: Path, jlink_version: str, module_jar: Path ): run_subprocess( [ self.jmod, "create", "--module-version", jlink_version, # Use LINUX-OTHER to be compatible with JDK 21+ b/294137077 "--target-platform", "LINUX-OTHER", "--class-path", module_jar, jmod_file, ] ) def link_jmods_into_jdk_image( self, jmod_dir: Path, module_name: str, out_dir: Path ): run_subprocess( [ self.jlink, "--module-path", jmod_dir, "--add-modules", module_name, "--output", out_dir, "--disable-plugin", "system-modules", ] ) def generate_module_descriptor(moduleName: str, jars: List[Path]) -> str: string_builder = "" string_builder += f"module {moduleName} {{\n" package_name_regex = r"(.*)/[^/]*.class" for jar in jars: with zipfile.ZipFile(jar) as jar_zip: entry_names = [entry.filename for entry in jar_zip.infolist()] package_names = set() for entry in entry_names: match = re.match(package_name_regex, entry) if match: package = match.group(1).replace("/", ".") package_names.add(package) for package in sorted(package_names): string_builder += f" exports {package};\n" string_builder += "}\n" return string_builder def create_jar( java: Path, jar_builder: Path, module_info_class: Path, in_jars: List[Path], output_jar: Path, ): # META: Our JarBuilder is similar to JarFlinger run_subprocess( [ java, "-jar", jar_builder, "--class-files", f"{module_info_class.name}:{module_info_class}", # META: Our jar builder only supports one append-jar "--append-jar", in_jars[0], "--output", output_jar, ] ) def copy_jrt_fs_jar(out_dir: Path, jdk_tools: JdkTools): source = jdk_tools.jrt_fs_location copied_libs_dir = out_dir / "lib" os.makedirs(copied_libs_dir, exist_ok=True) destination = copied_libs_dir / source.name # META: Skip manifest.mf filtering since it is not applicable to us shutil.copy(source, destination) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--core-for-system-modules-jar", required=True) parser.add_argument("--java", required=True) parser.add_argument("--javac", required=True) parser.add_argument("--jlink", required=True) parser.add_argument("--jmod", required=True) parser.add_argument("--jrt-fs-jar", required=True) parser.add_argument("--jar-builder", required=True) parser.add_argument("--output", required=True) args = parser.parse_args() jdk_tools = JdkTools( args.java, Path(args.javac), Path(args.jlink), Path(args.jmod), Path(args.jrt_fs_jar), Path(args.jar_builder), ) work_dir = tempfile.mkdtemp() JdkImageTransformDelegate( args.core_for_system_modules_jar, Path(work_dir), Path(args.output), jdk_tools, ).run() return 0 if __name__ == "__main__": sys.exit(main())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server