From 3a6f31e6b6bf7a3d9918ec4fdbf5d581d11de8a9 Mon Sep 17 00:00:00 2001 From: Vladimir Petko Date: Mon, 6 Jan 2025 15:17:08 +1300 Subject: [PATCH] feat: add jlink plugin Introduce jlink plugin to create Java runtime for the application. https://github.com/canonical/craft-parts/issues/891 --- craft_parts/plugins/jlink_plugin.py | 98 ++++++++++++++++++++++ craft_parts/plugins/plugins.py | 2 + tests/integration/plugins/test_jlink.py | 103 ++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 craft_parts/plugins/jlink_plugin.py create mode 100644 tests/integration/plugins/test_jlink.py diff --git a/craft_parts/plugins/jlink_plugin.py b/craft_parts/plugins/jlink_plugin.py new file mode 100644 index 000000000..8e8e92071 --- /dev/null +++ b/craft_parts/plugins/jlink_plugin.py @@ -0,0 +1,98 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2024 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +"""The JLink plugin.""" + +from typing import Literal, cast + +from overrides import override + +from .base import Plugin +from .properties import PluginProperties + + +class JLinkPluginProperties(PluginProperties, frozen=True): + """The part properties used by the JLink plugin.""" + + plugin: Literal["jlink"] = "jlink" + jlink_java_version: int = 21 + jlink_jars: list[str] = [] + + +class JLinkPlugin(Plugin): + """Create a Java Runtime using JLink.""" + + properties_class = JLinkPluginProperties + + @override + def get_build_packages(self) -> set[str]: + """Return a set of required packages to install in the build environment.""" + options = cast(JLinkPluginProperties, self._options) + return {f"openjdk-{options.jlink_java_version}-jdk"} + + @override + def get_build_environment(self) -> dict[str, str]: + """Return a dictionary with the environment to use in the build step.""" + return {} + + @override + def get_build_snaps(self) -> set[str]: + """Return a set of required snaps to install in the build environment.""" + return set() + + @override + def get_build_commands(self) -> list[str]: + """Return a list of commands to run during the build step.""" + options = cast(JLinkPluginProperties, self._options) + + commands = [] + + java_home = f"usr/lib/jvm/java-{options.jlink_java_version}-openjdk-${{CRAFT_TARGET_ARCH}}" + + if len(options.jlink_jars) > 0: + jars = " ".join(["${CRAFT_STAGE}/" + x for x in options.jlink_jars]) + commands.append(f"PROCESS_JARS={jars}") + else: + commands.append("PROCESS_JARS=$(find ${CRAFT_STAGE} -type f -name *.jar)") + + # create temp folder + commands.append("mkdir -p ${CRAFT_PART_BUILD}/tmp") + # extract jar files into temp folder + commands.append( + "(cd ${CRAFT_PART_BUILD}/tmp && for jar in ${PROCESS_JARS}; do jar xvf ${jar}; done;)" + ) + commands.append("CPATH=$(find ${CRAFT_PART_BUILD}/tmp -type f -name *.jar)") + commands.append("CPATH=$(echo ${CPATH}:. | sed s'/[[:space:]]/:/'g)") + commands.append("echo ${CPATH}") + commands.append( + f"""if [ "x${{PROCESS_JARS}}" != "x" ]; then + deps=$(jdeps --class-path=${{CPATH}} -q --recursive --ignore-missing-deps \ + --print-module-deps --multi-release {options.jlink_java_version} ${{PROCESS_JARS}}) + else + deps=java.base + fi + """ + ) + commands.append(f"INSTALL_ROOT=${{CRAFT_PART_INSTALL}}/{java_home}") + + commands.append( + "rm -rf ${INSTALL_ROOT} && jlink --add-modules ${deps} --output ${INSTALL_ROOT}" + ) + # create /usr/bin/java link + commands.append( + f"(cd ${{CRAFT_PART_INSTALL}} && mkdir -p usr/bin && ln -s --relative {java_home}/bin/java usr/bin/)" + ) + return commands diff --git a/craft_parts/plugins/plugins.py b/craft_parts/plugins/plugins.py index 1df175ef6..7d14b7974 100644 --- a/craft_parts/plugins/plugins.py +++ b/craft_parts/plugins/plugins.py @@ -27,6 +27,7 @@ from .dump_plugin import DumpPlugin from .go_plugin import GoPlugin from .go_use_plugin import GoUsePlugin +from .jlink_plugin import JLinkPlugin from .make_plugin import MakePlugin from .maven_plugin import MavenPlugin from .meson_plugin import MesonPlugin @@ -57,6 +58,7 @@ "dump": DumpPlugin, "go": GoPlugin, "go-use": GoUsePlugin, + "jlink": JLinkPlugin, "make": MakePlugin, "maven": MavenPlugin, "meson": MesonPlugin, diff --git a/tests/integration/plugins/test_jlink.py b/tests/integration/plugins/test_jlink.py new file mode 100644 index 000000000..685d5c335 --- /dev/null +++ b/tests/integration/plugins/test_jlink.py @@ -0,0 +1,103 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2024 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3 as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +import atexit +import os +import shutil +import subprocess +from pathlib import Path + +import yaml +import textwrap + +from craft_application.models import DEVEL_BASE_INFOS +from craft_parts import plugins +from craft_parts import LifecycleManager, Step + + +def test_jlink_plugin_with_jar(new_dir, partitions): + """Test that jlink produces tailored modules""" + + parts_yaml = textwrap.dedent( + f""" + parts: + my-part: + plugin: jlink + source: https://github.com/canonical/chisel-releases + source-type: git + source-branch: ubuntu-24.04 + jlink-jars: ["test.jar"] + after: ["stage-jar"] + stage-jar: + plugin: dump + source: . + """ + ) + parts = yaml.safe_load(parts_yaml) + + # build test jar + Path("Test.java").write_text( + """ + import javax.swing.*; + public class Test { + public static void main(String[] args) { + new JFrame("foo").setVisible(true); + } + } + """ + ) + subprocess.run(["javac", "Test.java"], check=True, capture_output=True) + subprocess.run( + ["jar", "cvf", "test.jar", "Test.class"], check=True, capture_output=True + ) + + lf = LifecycleManager( + parts, application_name="test_jlink", cache_dir=new_dir, partitions=partitions + ) + actions = lf.plan(Step.PRIME) + + with lf.action_executor() as ctx: + ctx.execute(actions) + + # java.desktop module should be included in the image + assert len(list(Path(f"{new_dir}/stage/usr/lib/jvm/").rglob("libawt.so"))) > 0 + + +def test_jlink_plugin_base(new_dir, partitions): + """Test that jlink produces base image""" + + parts_yaml = textwrap.dedent( + f""" + parts: + my-part: + plugin: jlink + source: "https://github.com/canonical/chisel-releases" + source-type: "git" + source-branch: "ubuntu-24.04" + """ + ) + parts = yaml.safe_load(parts_yaml) + + lf = LifecycleManager( + parts, application_name="test_jlink", cache_dir=new_dir, partitions=partitions + ) + actions = lf.plan(Step.PRIME) + + with lf.action_executor() as ctx: + ctx.execute(actions) + + java = new_dir / "stage/usr/bin/java" + assert java.isfile()