Skip to content

Commit

Permalink
feat: add Jlink plugin (#951)
Browse files Browse the repository at this point in the history
Co-authored-by: Tiago Nobrega <tiago.nobrega@canonical.com>
Co-authored-by: Michael DuBelko <michael.dubelko@gmail.com>
Co-authored-by: Callahan Kovacs <callahan.kovacs@canonical.com>
Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
  • Loading branch information
4 people authored Jan 30, 2025
1 parent c6dc566 commit ba3891a
Show file tree
Hide file tree
Showing 10 changed files with 498 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ jobs:
fi
echo "::endgroup::"
echo "::group::JDK"
sudo apt-get install -y openjdk-17-jdk-headless openjdk-21-jdk-headless openjdk-8-jdk-headless
sudo apt-get install -y openjdk-17-jdk openjdk-21-jdk openjdk-8-jdk-headless
echo "::endgroup::"
- name: Install uv
uses: astral-sh/setup-uv@v4
Expand Down
154 changes: 154 additions & 0 deletions craft_parts/plugins/jlink_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# -*- 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
from .validator import PluginEnvironmentValidator


class JLinkPluginProperties(PluginProperties, frozen=True):
"""The part properties used by the JLink plugin."""

plugin: Literal["jlink"] = "jlink"
jlink_jars: list[str] = []


class JLinkPluginEnvironmentValidator(PluginEnvironmentValidator):
"""Check the execution environment for the JLink plugin.
:param part_name: The part whose build environment is being validated.
:param env: A string containing the build step environment setup.
"""

@override
def validate_environment(
self, *, part_dependencies: list[str] | None = None
) -> None:
"""Ensure the environment contains dependencies needed by the plugin.
:param part_dependencies: A list of the parts this part depends on.
:raises PluginEnvironmentValidationError: If go is invalid
and there are no parts named go.
"""
self.validate_dependency(
dependency="jlink", plugin_name="jlink", part_dependencies=part_dependencies
)


class JLinkPlugin(Plugin):
"""Create a Java Runtime using JLink."""

properties_class = JLinkPluginProperties
validator_class = JLinkPluginEnvironmentValidator

@override
def get_build_packages(self) -> set[str]:
"""Return a set of required packages to install in the build environment."""
return set()

@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 = []

# Set JAVA_HOME to be used in jlink commands
commands.append(
"""
if [ -z "${JAVA_HOME+x}" ]; then
JAVA_HOME=$(dirname $(dirname $(readlink -f $(which jlink))))
fi
if [ ! -x "${JAVA_HOME}/bin/java" ]; then
echo "Error: JAVA_HOME: '${JAVA_HOME}/bin/java' is not an executable." >&2
exit 1
fi
JLINK=${JAVA_HOME}/bin/jlink
JDEPS=${JAVA_HOME}/bin/jdeps
"""
)

# extract jlink version and use it to define the destination
# and multi-release jar version for the dependency enumeration
commands.append("JLINK_VERSION=$(${JLINK} --version)")
commands.append(
"DEST=usr/lib/jvm/java-${JLINK_VERSION%%.*}-openjdk-${CRAFT_ARCH_BUILD_FOR}"
)
commands.append("MULTI_RELEASE=${JLINK_VERSION%%.*}")

# find application jars - either all jars in the staging area
# or a list specified in jlink_jars option
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 - spring boot fat jar
# contains dependent jars inside
commands.append(
"(cd ${CRAFT_PART_BUILD}/tmp && for jar in ${PROCESS_JARS}; do jar xvf ${jar}; done;)"
)
# create classpath - add all dependent jars and all staged jars
commands.append("CPATH=.")
commands.append(
"""
find ${CRAFT_PART_BUILD}/tmp -type f -name *.jar | while IFS= read -r file; do
CPATH=$CPATH:${file}
done
find ${CRAFT_STAGE} -type f -name *.jar | while IFS= read -r file; do
CPATH=$CPATH:${file}
done
"""
)
commands.append(
"""if [ "x${PROCESS_JARS}" != "x" ]; then
deps=$(${JDEPS} --class-path=${CPATH} -q --recursive --ignore-missing-deps \
--print-module-deps --multi-release ${MULTI_RELEASE} ${PROCESS_JARS})
else
deps=java.base
fi
"""
)
commands.append("INSTALL_ROOT=${CRAFT_PART_INSTALL}/${DEST}")

commands.append(
"rm -rf ${INSTALL_ROOT} && ${JLINK} --no-header-files --no-man-pages --strip-debug --add-modules ${deps} --output ${INSTALL_ROOT}"
)
# create /usr/bin/java link
commands.append(
"(cd ${CRAFT_PART_INSTALL} && mkdir -p usr/bin && ln -s --relative ${DEST}/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
2 changes: 1 addition & 1 deletion craft_parts/sources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@


def _detect_source_type(
data: SourceModel | dict[str, Any]
data: SourceModel | dict[str, Any],
) -> SourceModel | dict[str, Any]:
"""Get the source type for a source if it's not already provided."""
if isinstance(data, BaseSourceModel) or "source-type" in data:
Expand Down
10 changes: 10 additions & 0 deletions docs/common/craft-parts/craft-parts.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ InvalidSourceOption
InvalidSourceOptions
InvalidSourceType
Iterable
JAR
JavaPlugin
JLink
JLinkPlugin
JLinkPluginEnvironmentValidator
JLinkPluginProperties
JSON
LDFLAGS
LLVM
Expand Down Expand Up @@ -145,6 +150,7 @@ NpmPlugin
NpmPluginEnvironmentValidator
NpmPluginProperties
OCI
OpenJDK
OSError
OsRelease
OsReleaseCodenameError
Expand Down Expand Up @@ -313,6 +319,7 @@ chmod
chroot
chrooted
classmethod
classpath
classvar
cli
cls
Expand Down Expand Up @@ -368,7 +375,9 @@ ing
initialized
iterable
iojs
jdeps
jdk
jlink
jre
js
json
Expand Down Expand Up @@ -398,6 +407,7 @@ oci
ok
onboarding
opencontainers
openjdk
organization
organize
organized
Expand Down
107 changes: 107 additions & 0 deletions docs/common/craft-parts/reference/plugins/jlink_plugin.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
.. _craft_parts_jlink_plugin:

JLink plugin
=============

The `JLink <jlink_>`_ plugin can be used for Java projects where
you would want to deploy a Java runtime specific for your application
or install a minimal Java runtime.


Keywords
--------

This plugin uses the common :ref:`plugin <part-properties-plugin>` keywords as
well as those for :ref:`sources <part-properties-sources>`.

Additionally, this plugin provides the plugin-specific keywords defined in the
following sections.

jlink-jars
~~~~~~~~~~~~~~~~~~
**Type:** list of strings

List of paths to your application's JAR files. If not specified, the plugin
will find all JAR files in the staging area.

Dependencies
------------

The plugin expects OpenJDK to be available on the system and to contain
the ``jlink`` executable. OpenJDK can be defined as a
``build-package`` in the part using ``jlink`` plugin.
Another alternative is to define another part with the name
``jlink-deps``, and declare that the part using the
``jlink`` plugin comes :ref:`after <after>` the ``jlink-deps`` part.

If the system has multiple OpenJDK installations available, one
must be selected by setting the ``JAVA_HOME`` environment variable.

.. code-block:: yaml
parts:
runtime:
plugin: jlink
build-packages:
- openjdk-21-jdk
build-environment:
- JAVA_HOME: /usr/jvm/java-21-openjdk-${CRAFT_ARCH_BUILD_FOR}
The user is expected to stage OpenJDK dependencies either by installing
an appropriate OpenJDK slice:

.. code-block:: yaml
parts:
runtime:
plugin: jlink
build-packages:
- openjdk-21-jdk
after:
- deps
deps:
plugin: nil
stage-packages:
- openjdk-21-jre-headless_security
stage:
- -usr/lib/jvm
Or, by installing the dependencies directly:

.. code-block:: yaml
parts:
runtime:
plugin: jlink
build-packages:
- openjdk-21-jdk
after:
- deps
deps:
plugin: nil
stage-packages:
- libc6_libs
- libgcc-s1_libs
- libstdc++6_libs
- zlib1g_libs
- libnss3_libs
How it works
------------

During the build step, the plugin performs the following actions:

* Finds all JAR files in the staging area or selects jars specified in
``jlink-jars``.
* Unpacks JAR files to the temporary location and concatenates all embedded jars
into `jdeps <jdeps_>`_ classpath.
* Runs `jdeps <jdeps_>`_ to discover Java modules required for the staged jars.
* Runs `jlink <jlink_>`_ to create a runtime image from the build JDK.


.. _`jdeps`: https://docs.oracle.com/en/java/javase/21/docs/specs/man/jdeps.html
.. _`jlink`: https://docs.oracle.com/en/java/javase/21/docs/specs/man/jlink.html
8 changes: 8 additions & 0 deletions docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
Changelog
*********

2.5.0 (2025-XX-XX)
------------------

New features:

- Add the :ref:`jlink plugin<craft_parts_jlink_plugin>` for setting up
Java runtime.

2.4.1 (2025-01-24)
------------------

Expand Down
1 change: 1 addition & 0 deletions docs/reference/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ lifecycle.
/common/craft-parts/reference/plugins/dump_plugin.rst
/common/craft-parts/reference/plugins/go_plugin.rst
/common/craft-parts/reference/plugins/go_use_plugin.rst
/common/craft-parts/reference/plugins/jlink_plugin.rst
/common/craft-parts/reference/plugins/make_plugin.rst
/common/craft-parts/reference/plugins/maven_plugin.rst
/common/craft-parts/reference/plugins/meson_plugin.rst
Expand Down
Loading

0 comments on commit ba3891a

Please sign in to comment.