Skip to content

Commit

Permalink
feat: add jlink plugin
Browse files Browse the repository at this point in the history
Introduce jlink plugin to create Java runtime
for the application.
canonical#891
  • Loading branch information
vpa1977 committed Jan 6, 2025
1 parent 1a2631e commit 3a6f31e
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 0 deletions.
98 changes: 98 additions & 0 deletions craft_parts/plugins/jlink_plugin.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

"""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
2 changes: 2 additions & 0 deletions craft_parts/plugins/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,6 +58,7 @@
"dump": DumpPlugin,
"go": GoPlugin,
"go-use": GoUsePlugin,
"jlink": JLinkPlugin,
"make": MakePlugin,
"maven": MavenPlugin,
"meson": MesonPlugin,
Expand Down
103 changes: 103 additions & 0 deletions tests/integration/plugins/test_jlink.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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()

0 comments on commit 3a6f31e

Please sign in to comment.