See https://openjdk.org/jeps/238

This commit adds basic support for building multi-release jar files.
A multi-release jar file has release-specific classes in directories
under META-INF/versions/ and its MANIFEST.MF contains a line with
'Multi-Release: true'.

The probably most common case of a multi-release jar file has only one
single such class which is 'META-INF/versions/9/module-info.class'.

To do so, we add JAVA_RELEASE_SRC_DIRS as a new eclass variable which
is also used as the condition to trigger the new functionality. A new
local variable 'multi_release' is added to the packaging section (the
part using the 'jar -create' command). Only when JAVA_RELEASE_SRC_DIRS
is set, additional actions take place:

- Compilation (those are the parts with 'ejavac') will additionally loop
  over the release-specific directories listed in JAVA_RELEASE_SRC_DIRS
  and compile the release-specific classes into corresponding directories
  under target/versions/.

- Packaging (the part using the 'jar -create' command) will add the
  details to the 'multi_release' variable so that the release-specific
  directories under target/versions/ can be packaged into the jar file.

This commit also adds funtionality to generate 'module-info.java' files.
It is useful for packages where module-info.java is not provided in the
sources but needs to be generated by the build system. We use the built-in
jdeps function with the --generate-module-info option which became available
with Java 11.

It generates the module-info.java file based on an intermediate jar file
and places it in the "${JAVA_MODULE_INFO_OUT}/${JAVA_INTERMEDIATE_JAR_NAME}/"
directory.

For this purpose we add three new eclass variables:
- JAVA_INTERMEDIATE_JAR_NAME
- JAVA_MODULE_INFO_OUT
- JAVA_MODULE_INFO_RELEASE

When both JAVA_MODULE_INFO_OUT and JAVA_INTERMEDIATE_JAR_NAME are defined in the
ebuild we
- compile the sources still without module-info
- package them as an intermediate {JAVA_INTERMEDIATE_JAR_NAME}.jar
- let java-pkg-simple_generate-module-info generate the module-info
- compile module-info.java with the --patch-module option
- package the final jar file including the module-info.class

When the JAVA_MODULE_INFO_RELEASE variable is set, module-info.java is
generated into a release specific directory
"${JAVA_MODULE_INFO_OUT}/${JAVA_INTERMEDIATE_JAR_NAME}/versions/{JAVA_MODULE_INFO_RELEASE}".

Bug: https://bugs.gentoo.org/900433
Signed-off-by: Volkmar W. Pogatzki <gen...@pogatzki.net>
---
 eclass/java-pkg-simple.eclass | 269 +++++++++++++++++++++++++++++++++-
 1 file changed, 264 insertions(+), 5 deletions(-)

diff --git a/eclass/java-pkg-simple.eclass b/eclass/java-pkg-simple.eclass
index ce4a62f048da..84384116af99 100644
--- a/eclass/java-pkg-simple.eclass
+++ b/eclass/java-pkg-simple.eclass
@@ -1,4 +1,4 @@
-# Copyright 2004-2024 Gentoo Authors
+# Copyright 2004-2025 Gentoo Authors
 # Distributed under the terms of the GNU General Public License v2

 # @ECLASS: java-pkg-simple.eclass
@@ -11,7 +11,9 @@
 # @DESCRIPTION:
 # This class is intended to build pure Java packages from Java sources
 # without the use of any build instructions shipped with the sources.
-# There is no support for generating source files, or for controlling
+# It can generate module-info.java files and supports adding the Main-Class
+# and the Automatic-Module-Name attributes to MANIFEST.MF. There is no
+# further support for generating source files, or for controlling
 # the META-INF of the resulting jar, although these issues may be
 # addressed by an ebuild by putting corresponding files into the target
 # directory before calling the src_compile function of this eclass.
@@ -111,7 +113,6 @@ fi
 #      )
 # @CODE

-# @DESCRIPTION:
 # @ECLASS_VARIABLE: JAVA_RESOURCE_DIRS
 # @DEFAULT_UNSET
 # @DESCRIPTION:
@@ -225,6 +226,50 @@ fi
 # @DESCRIPTION:
 # It is almost equivalent to ${JAVA_RESOURCE_DIRS} in src_test.

+# @ECLASS_VARIABLE: JAVA_INTERMEDIATE_JAR_NAME
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# Name of the intermediate jar file excluding the '.jar' suffix and also name 
of the
+# ejavac output directory which are needed by 'jdeps --generate-module-info'.
+# @CODE
+# Examples:
+#      JAVA_INTERMEDIATE_JAR_NAME="org.apache.${PN/-/.}"
+#      JAVA_INTERMEDIATE_JAR_NAME="com.github.marschall.memoryfilesystem"
+# @CODE
+
+# @ECLASS_VARIABLE: JAVA_MODULE_INFO_OUT
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# Used by java-pkg-simple_generate-module-info.
+# It is the directory where module-info.java will be created.
+# Only when this variable is set, module-info.java will be created.
+# @CODE
+# Example:
+#      JAVA_MODULE_INFO_OUT="src/main"
+# @CODE
+
+# @ECLASS_VARIABLE: JAVA_MODULE_INFO_RELEASE
+# @DESCRIPTION:
+# Used by java-pkg-simple_generate-module-info.
+# Correlates to JAVA_RELEASE_SRC_DIRS.
+# When this variable is set, module-info.java will be placed in
+# 
${JAVA_MODULE_INFO_OUT}/${JAVA_INTERMEDIATE_JAR_NAME}/versions/${JAVA_MODULE_INFO_RELEASE}
+
+# @ECLASS_VARIABLE: JAVA_RELEASE_SRC_DIRS
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# An associative array of directories with release-specific sources which are
+# used for building multi-release jar files.
+# @CODE
+# Example:
+#      JAVA_RELEASE_SRC_DIRS=(
+#              ["9"]="prov/src/main/jdk1.9"
+#              ["11"]="prov/src/main/jdk1.11"
+#              ["15"]="prov/src/main/jdk1.15"
+#              ["21"]="prov/src/main/jdk21"
+#      )
+# @CODE
+
 # @FUNCTION: java-pkg-simple_getclasspath
 # @USAGE: java-pkg-simple_getclasspath
 # @INTERNAL
@@ -276,6 +321,88 @@ java-pkg-simple_getclasspath() {
        debug-print "CLASSPATH=${classpath}"
 }

+# @FUNCTION: java-pkg-simple_getmodulepath
+# @USAGE: java-pkg-simple_getmodulepath
+# @INTERNAL
+# @DESCRIPTION:
+# Cloned from java-pkg-simple_getclasspath, dropped 'deep_jars'
+# and replaced s/classpath/modulepath/g.
+#
+# It is needed for java-pkg-simple_generate-module-info where using classpath
+# would cause problems with '--with-dependencies'.
+# And it is also used for compilation.
+#
+# Note that the variable "modulepath" needs to be defined before
+# calling this function.
+java-pkg-simple_getmodulepath() {
+       debug-print-function ${FUNCNAME} $*
+
+       local dependency
+       local buildonly_jars="--build-only"
+
+       # the extra classes that are not installed by portage
+       modulepath+=":${JAVA_GENTOO_CLASSPATH_EXTRA}"
+
+       # the extra classes that are installed by portage
+       for dependency in ${JAVA_CLASSPATH_EXTRA}; do
+               modulepath="${modulepath}:$(java-pkg_getjars ${buildonly_jars} \
+                       ${dependency})"
+       done
+
+       # add test dependencies if USE FLAG 'test' is set
+       if has test ${JAVA_PKG_IUSE} && use test; then
+               for dependency in ${JAVA_TEST_GENTOO_CLASSPATH}; do
+                       modulepath="${modulepath}:$(java-pkg_getjars 
${buildonly_jars} \
+                               ${dependency})"
+               done
+       fi
+
+       # add the RUNTIME dependencies
+       for dependency in ${JAVA_GENTOO_CLASSPATH}; do
+               modulepath="${modulepath}:$(java-pkg_getjars ${dependency})"
+       done
+
+       # purify modulepath
+       while [[ $modulepath = *::* ]]; do modulepath="${modulepath//::/:}"; 
done
+       modulepath=${modulepath%:}
+       modulepath=${modulepath#:}
+
+       debug-print "modulepath=${modulepath}"
+}
+
+# @FUNCTION: java-pkg-simple_generate-module-info
+# @USAGE: java-pkg-simple_generate-module-info
+# @INTERNAL
+# @DESCRIPTION:
+# Calls jdeps --generate-module-info which generates module-info.java.
+# Requires an intermediate jar file to be named as 
"${JAVA_INTERMEDIATE_JAR_NAME}.jar".
+java-pkg-simple_generate-module-info() {
+       debug-print-function ${FUNCNAME} $*
+
+       local modulepath="" jdeps_args=""
+       java-pkg-simple_getmodulepath
+
+       # Default to release 9 in order to avoid having to set it in the ebuild.
+       : "${JAVA_MODULE_INFO_RELEASE:=9}"
+
+       if [[ ${JAVA_MODULE_INFO_RELEASE} ]]; then
+               jdeps_args="${jdeps_args} --multi-release 
${JAVA_MODULE_INFO_RELEASE}"
+       fi
+
+       if [[ ${modulepath} ]]; then
+               jdeps_args="${jdeps_args} --module-path ${modulepath}"
+               jdeps_args="${jdeps_args} --add-modules=ALL-MODULE-PATH"
+       fi
+       debug-print "jdeps_args is ${jdeps_args}"
+
+       jdeps \
+               --generate-module-info "${JAVA_MODULE_INFO_OUT}" \
+               ${jdeps_args} \
+               "${JAVA_INTERMEDIATE_JAR_NAME}.jar" || die
+
+       moduleinfo=$(find -type f -name module-info.java)
+}
+
 # @FUNCTION: java-pkg-simple_test_with_pkgdiff_
 # @INTERNAL
 # @DESCRIPTION:
@@ -374,6 +501,117 @@ java-pkg-simple_src_compile() {
                java-pkg_gen-cp JAVA_GENTOO_CLASSPATH
        fi

+       # generate module-info.java only if JAVA_MODULE_INFO_OUT is defined in 
the ebuild
+       if [[ ${JAVA_MODULE_INFO_OUT} && ${JAVA_INTERMEDIATE_JAR_NAME} ]]; then
+
+               local jdk="$(depend-java-query --get-lowest "${DEPEND}")"
+               if [[ "${jdk#1.}" -lt 9 ]]; then
+                       die "Wrong DEPEND, needs at least virtual/jdk-9"
+               fi
+
+               local classpath=""
+               java-pkg-simple_getclasspath
+
+               # gather sources and compile classes for the intermediate jar 
file
+               find "${JAVA_SRC_DIR[@]}" -name \*.java ! -name 
module-info.java > ${sources}
+               ejavac -d ${classes} -encoding ${JAVA_ENCODING}\
+                       ${classpath:+-classpath ${classpath}} ${JAVAC_ARGS} 
@${sources}
+
+               java-pkg-simple_prepend_resources ${classes} 
"${JAVA_RESOURCE_DIRS[@]}"
+
+               # package the intermediate jar file
+               # The intermediate jar file is a precondition for jdeps to 
generate
+               # a module-info.java file.
+               jar cvf "${JAVA_INTERMEDIATE_JAR_NAME}.jar" \
+                       -C target/classes . || die
+
+               # now, generate module-info.java
+               java-pkg-simple_generate-module-info
+               debug-print "generated moduleinfo is ${moduleinfo}"
+
+               # If JAVA_RELEASE_SRC_DIRS was not set in the ebuild, set it 
now:
+               if [[ ${JAVA_MODULE_INFO_RELEASE} && -z 
${JAVA_RELEASE_SRC_DIRS[@]} ]]; then
+                       # TODO: use JAVA_MODULE_INFO_RELEASE instead of fixed 
value.
+                       JAVA_RELEASE_SRC_DIRS=( 
["9"]=${JAVA_MODULE_INFO_OUT}/${JAVA_INTERMEDIATE_JAR_NAME}"/versions/9" )
+               fi
+       fi
+
+       # JEP 238 multi-release support, https://openjdk.org/jeps/238 #900433
+       #
+       # Basic support for building multi-release jar files according to JEP 
238.
+       # A multi-release jar file has release-specific classes in directories
+       # under META-INF/versions/.
+       # Its META-INF/MANIFEST.MF contains the line: 'Multi-Release: true'.
+       if [[ -n ${JAVA_RELEASE_SRC_DIRS[@]} ]]; then
+               # Ensure correct virtual/jdk version
+               # Initialize a variable to track the highest key
+               local highest_version=-1
+
+               # Loop through the keys of the associative array
+               for key in "${!JAVA_RELEASE_SRC_DIRS[@]}"; do
+                   # Compare the numeric value of the key
+                   if [[ key > highest_version ]]; then
+                       highest_version="$key"
+                   fi
+               done
+
+               local jdk="$(depend-java-query --get-lowest "${DEPEND}")"
+               if [[ "${jdk#1.}" -lt "${highest_version}" ]]; then
+                       die "Wrong DEPEND, needs at least 
virtual/jdk-${highest_version}"
+               fi
+
+               local classpath=""
+               java-pkg-simple_getclasspath
+
+               # An intermediate jar file might already exist from generation 
of the
+               # module-info.java file
+               if [[ ! $(find . -name ${JAVA_INTERMEDIATE_JAR_NAME}.jar) ]]; 
then
+                       einfo "generating intermediate for multi-release"
+                       # gather sources and compile classes for the 
intermediate jar file
+                       find "${JAVA_SRC_DIR[@]}" -name \*.java ! -name 
module-info.java > ${sources}
+                       ejavac -d ${classes} -encoding ${JAVA_ENCODING}\
+                               ${classpath:+-classpath ${classpath}} 
${JAVAC_ARGS} @${sources}
+
+                       java-pkg-simple_prepend_resources ${classes} 
"${JAVA_RESOURCE_DIRS[@]}"
+
+                       # package the intermediate jar file
+                       # The intermediate jar file is a precondition for jdeps 
to generate
+                       # a module-info.java file.
+                       jar cvf "${JAVA_INTERMEDIATE_JAR_NAME}.jar" \
+                               -C target/classes . || die
+               fi
+
+               local tmp_source=${JAVA_PKG_WANT_SOURCE} 
tmp_target=${JAVA_PKG_WANT_TARGET}
+
+               # compile content of release-specific source directories
+               local version
+               for version in "${!JAVA_RELEASE_SRC_DIRS[@]}"; do
+                       local release="${version}"
+                       local reldir="${JAVA_RELEASE_SRC_DIRS[${version}]}"
+                       debug-print "Release is ${release}, directory is 
${reldir}"
+
+                       JAVA_PKG_WANT_SOURCE="${release}"
+                       JAVA_PKG_WANT_TARGET="${release}"
+
+                       local modulepath=""
+                       java-pkg-simple_getmodulepath
+
+                       # compile sources in ${reldir}
+                       ejavac \
+                               -d target/versions/${release} \
+                               -encoding ${JAVA_ENCODING} \
+                               -classpath 
"${modulepath}:${JAVA_INTERMEDIATE_JAR_NAME}.jar" \
+                               --module-path 
"${modulepath}:${JAVA_INTERMEDIATE_JAR_NAME}.jar" \
+                               --module-version ${PV} \
+                               --patch-module 
"${JAVA_INTERMEDIATE_JAR_NAME}"="${JAVA_INTERMEDIATE_JAR_NAME}.jar" \
+                               ${JAVAC_ARGS} $(find ${reldir} -type f -name 
'*.java')
+
+                       
JAVA_GENTOO_CLASSPATH_EXTRA+=":target/versions/${release}"
+               done
+
+               JAVA_PKG_WANT_SOURCE=${tmp_source}
+               JAVA_PKG_WANT_TARGET=${tmp_target}
+       else
                # gather sources
                # if target < 9, we need to compile module-info.java separately
                # as this feature is not supported before Java 9
@@ -420,6 +658,7 @@ java-pkg-simple_src_compile() {
                                eqawarn "Please adjust DEPEND accordingly. See 
https://bugs.gentoo.org/796875#c3";
                        fi
                fi
+       fi

        # javadoc
        if has doc ${JAVA_PKG_IUSE} && use doc; then
@@ -442,14 +681,29 @@ java-pkg-simple_src_compile() {
        fi

        # package
-       local jar_args
+       local jar_args multi_release=""
+       if [[ -n ${JAVA_RELEASE_SRC_DIRS[@]} ]]; then
+               # Preparing the multi_release variable. From multi-release 
compilation
+               # the release-specific classes are sorted in 
target/versions/${release}
+               # directories.
+
+               # TODO:
+               # Could this possibly be simplified with printf?
+               pushd target/versions >> /dev/null || die
+                       for version in $(ls -d * | sort -g); do
+                               debug-print "Version is ${version}"
+                               multi_release="${multi_release} --release 
${version} -C target/versions/${version} . "
+                       done
+               popd >> /dev/null || die
+       fi
+
        if [[ -e ${classes}/META-INF/MANIFEST.MF ]]; then
                sed '/Created-By: /Id' -i ${classes}/META-INF/MANIFEST.MF
                jar_args="cfm ${JAVA_JAR_FILENAME} 
${classes}/META-INF/MANIFEST.MF"
        else
                jar_args="cf ${JAVA_JAR_FILENAME}"
        fi
-       jar ${jar_args} -C ${classes} . || die "jar failed"
+       jar ${jar_args} -C ${classes} . ${multi_release} || die "jar failed"
        if  [[ -n "${JAVA_AUTOMATIC_MODULE_NAME}" ]]; then
                echo "Automatic-Module-Name: ${JAVA_AUTOMATIC_MODULE_NAME}" \
                        >> "${T}/add-to-MANIFEST.MF" || die "adding module name 
failed"
@@ -463,6 +717,11 @@ java-pkg-simple_src_compile() {
                        || die "updating MANIFEST.MF failed"
                rm -f "${T}/add-to-MANIFEST.MF" || die "cannot remove"
        fi
+
+       unset JAVA_INTERMEDIATE_JAR_NAME
+       unset JAVA_MODULE_INFO_OUT
+       unset JAVA_MODULE_INFO_RELEASE
+       unset JAVA_RELEASE_SRC_DIRS
 }

 # @FUNCTION: java-pkg-simple_src_install
--
2.41.0


Reply via email to