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