From 1c3b3d28f00da5fa869f11afa7a99d6cbf97b6a9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:39:15 -0400 Subject: [PATCH] Support Temurin JDKs with JMOD files (#1149) * Initial plan * Add Temurin JMOD installation support * Rebuild action bundles * Use java-package for Temurin JMODs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d8c581-2d14-4ccc-aeca-afc0f3b0c2bc * Fix Temurin JMOD test paths on Windows Use platform-aware path construction for the JMOD copy and cache assertions so the Windows test expects backslash-normalized paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Bruno Borges Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e7d8c581-2d14-4ccc-aeca-afc0f3b0c2bc --- README.md | 2 +- .../distributors/distribution-factory.test.ts | 16 +++ .../distributors/temurin-installer.test.ts | 96 +++++++++++++++- action.yml | 2 +- dist/setup/index.js | 65 +++++++---- docs/advanced-usage.md | 1 + src/distributions/distribution-factory.ts | 9 ++ src/distributions/temurin/installer.ts | 106 +++++++++++++----- 8 files changed, 244 insertions(+), 53 deletions(-) create mode 100644 __tests__/distributors/distribution-factory.test.ts diff --git a/README.md b/README.md index ded2fa82..2218c754 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ For more details, see the full release notes on the [releases page](https://git - `distribution`: Java [distribution](#supported-distributions). Required unless `java-version-file` points to `.sdkmanrc` with a recognized distribution suffix (for example `java=21.0.5-tem`). - - `java-package`: The packaging variant of the chosen distribution. Possible values: `jdk`, `jre`, `jdk+fx`, `jre+fx`. For Azul Zulu, `jdk+crac` and `jre+crac` are also supported. Default value: `jdk`. + - `java-package`: The packaging variant of the chosen distribution. Possible values: `jdk`, `jre`, `jdk+fx`, `jre+fx`. For Azul Zulu, `jdk+crac` and `jre+crac` are also supported. For Eclipse Temurin 24 and later, `jdk+jmods` includes the separately packaged JMOD files. Default value: `jdk`. - `architecture`: The target architecture of the package. Possible values: `x86`, `x64`, `armv7`, `aarch64`, `ppc64le`. Default value: Derived from the runner machine. diff --git a/__tests__/distributors/distribution-factory.test.ts b/__tests__/distributors/distribution-factory.test.ts new file mode 100644 index 00000000..4da7b23c --- /dev/null +++ b/__tests__/distributors/distribution-factory.test.ts @@ -0,0 +1,16 @@ +import {getJavaDistribution} from '../../src/distributions/distribution-factory.js'; + +describe('getJavaDistribution', () => { + it("rejects java-package 'jdk+jmods' for non-Temurin distributions", () => { + expect(() => + getJavaDistribution('zulu', { + version: '25', + architecture: 'x64', + packageType: 'jdk+jmods', + checkLatest: false + }) + ).toThrow( + "java-package 'jdk+jmods' is only supported for distribution 'temurin'." + ); + }); +}); diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index dd2d73a5..06b5ce0b 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -13,6 +13,7 @@ import type {TemurinImplementation as TemurinImplementationType} from '../../src import {HttpClient} from '@actions/http-client'; import fs from 'fs'; import os from 'os'; +import path from 'path'; import manifestData from '../data/temurin.json' with {type: 'json'}; @@ -119,6 +120,16 @@ describe('getAvailableVersions', () => { TemurinImplementation.Hotspot, 'os=mac&architecture=x64&image_type=jdk&release_type=ga&jvm_impl=hotspot&page_size=20&page=0' ], + [ + { + version: '25', + architecture: 'x64', + packageType: 'jdk+jmods', + checkLatest: false + }, + TemurinImplementation.Hotspot, + 'os=mac&architecture=x64&image_type=jdk&release_type=ga&jvm_impl=hotspot&page_size=20&page=0' + ], [ { version: '16', @@ -169,6 +180,27 @@ describe('getAvailableVersions', () => { } ); + it('requests the JMOD image type', async () => { + const distribution = new TemurinDistribution( + { + version: '25', + architecture: 'x64', + packageType: 'jdk+jmods', + checkLatest: false + }, + TemurinImplementation.Hotspot + ); + distribution['getPlatformOption'] = () => 'linux'; + + await distribution['getAvailableVersions']('jmods'); + + expect(spyHttpClient).toHaveBeenCalledWith( + expect.stringContaining( + 'os=linux&architecture=x64&image_type=jmods&release_type=ga' + ) + ); + }); + it('load available versions', async () => { const nextPageUrl = 'https://api.adoptium.net/v3/assets/version/%5B1.0,100.0%5D?page=1&page_size=20'; @@ -229,7 +261,12 @@ describe('getAvailableVersions', () => { it.each([ [TemurinImplementation.Hotspot, 'jdk', 'Java_Temurin-Hotspot_jdk'], - [TemurinImplementation.Hotspot, 'jre', 'Java_Temurin-Hotspot_jre'] + [TemurinImplementation.Hotspot, 'jre', 'Java_Temurin-Hotspot_jre'], + [ + TemurinImplementation.Hotspot, + 'jdk+jmods', + 'Java_Temurin-Hotspot_jdk+jmods' + ] ])( 'find right toolchain folder', ( @@ -386,6 +423,7 @@ describe('downloadTool', () => { let spyCacheDir: any; let spyReadDirSync: any; let spyRenameWinArchive: any; + let spyCopySync: any; beforeEach(() => { spyDownloadTool = tc.downloadTool as jest.Mock; @@ -400,6 +438,8 @@ describe('downloadTool', () => { spyReadDirSync.mockReturnValue(['jdk-17'] as any); spyRenameWinArchive = util.renameWinArchive as jest.Mock; spyRenameWinArchive.mockReturnValue('/tmp/jdk.tar.gz.zip'); + spyCopySync = jest.spyOn(fs, 'cpSync'); + spyCopySync.mockImplementation(() => undefined); }); afterEach(() => { @@ -433,6 +473,60 @@ describe('downloadTool', () => { ); }); + it('downloads and adds matching JMODs to the JDK', async () => { + spyDownloadTool + .mockResolvedValueOnce('/tmp/jdk.tar.gz') + .mockResolvedValueOnce('/tmp/jmods.tar.gz'); + spyExtractJdkFile + .mockResolvedValueOnce('/tmp/extracted') + .mockResolvedValueOnce('/tmp/extracted-jmods'); + spyReadDirSync + .mockReturnValueOnce(['jdk-25'] as any) + .mockReturnValueOnce(['jdk-25-jmods'] as any); + jest.spyOn(fs, 'existsSync').mockReturnValue(false); + + const distribution = new TemurinDistribution( + { + version: '25', + architecture: 'x64', + packageType: 'jdk+jmods', + checkLatest: false + }, + TemurinImplementation.Hotspot + ); + distribution['resolvePackage'] = jest.fn().mockResolvedValue({ + version: '25.0.3+9', + url: 'https://example.com/jmods.tar.gz' + }); + + await distribution['downloadTool']({ + version: '25.0.3+9', + url: 'https://example.com/jdk.tar.gz' + }); + + expect(distribution['resolvePackage']).toHaveBeenCalledWith( + '25.0.3+9', + 'jmods' + ); + expect(spyDownloadTool).toHaveBeenNthCalledWith( + 2, + 'https://example.com/jmods.tar.gz' + ); + expect(spyCopySync).toHaveBeenCalledWith( + path.join('/tmp/extracted-jmods', 'jdk-25-jmods'), + process.platform === 'darwin' + ? path.join('/tmp/extracted', 'jdk-25', 'Contents', 'Home', 'jmods') + : path.join('/tmp/extracted', 'jdk-25', 'jmods'), + {recursive: true} + ); + expect(spyCacheDir).toHaveBeenCalledWith( + path.join('/tmp/extracted', 'jdk-25'), + 'Java_Temurin-Hotspot_jdk+jmods', + '25.0.3-9', + 'x64' + ); + }); + it('fails when signature is missing and verification is enabled', async () => { const distribution = new TemurinDistribution( { diff --git a/action.yml b/action.yml index 2412e840..901c198a 100644 --- a/action.yml +++ b/action.yml @@ -13,7 +13,7 @@ inputs: description: 'Java distribution. See the list of supported distributions in README file. This input is required except when java-version-file points to .sdkmanrc with a recognized distribution suffix (e.g., java=21.0.5-tem).' required: false java-package: - description: 'The package type (jdk, jre, jdk+fx, jre+fx, jdk+crac, jre+crac)' + description: 'The package type (jdk, jre, jdk+fx, jre+fx, jdk+crac, jre+crac, jdk+jmods)' required: false default: 'jdk' architecture: diff --git a/dist/setup/index.js b/dist/setup/index.js index d311f7fe..69379a0e 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -129873,21 +129873,27 @@ wI4qF/KKq9BfyfucAs0ykA== + var TemurinImplementation; (function (TemurinImplementation) { TemurinImplementation["Hotspot"] = "Hotspot"; })(TemurinImplementation || (TemurinImplementation = {})); class TemurinDistribution extends JavaBase { jvmImpl; + includeJmods; constructor(installerOptions, jvmImpl) { super(`Temurin-${jvmImpl}`, installerOptions); this.jvmImpl = jvmImpl; + this.includeJmods = this.packageType === 'jdk+jmods'; } /** * @internal For cross-distribution reuse only. Not intended as a public API. */ async findPackageForDownload(version) { - const availableVersionsRaw = await this.getAvailableVersions(); + return this.resolvePackage(version, this.includeJmods ? 'jdk' : this.packageType); + } + async resolvePackage(version, imageType) { + const availableVersionsRaw = await this.getAvailableVersions(imageType); const availableVersionsWithBinaries = availableVersionsRaw .filter(item => item.binaries.length > 0) .map(item => { @@ -129915,19 +129921,7 @@ class TemurinDistribution extends JavaBase { } async downloadTool(javaRelease) { info(`Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...`); - let javaArchivePath = await downloadTool(javaRelease.url); - if (this.verifySignature) { - if (!javaRelease.signatureUrl) { - throw new Error(`Input 'verify-signature' is enabled, but no signature URL was found for Temurin version ${javaRelease.version}.`); - } - info(`Verifying Java package signature...`); - try { - await verifyPackageSignature(javaArchivePath, javaRelease.signatureUrl, this.verifySignaturePublicKey ?? ADOPTIUM_PUBLIC_KEY); - } - catch (error) { - throw new Error(`Failed to verify signature for Temurin version ${javaRelease.version} from ${javaRelease.signatureUrl}: ${error.message}`, { cause: error }); - } - } + let javaArchivePath = await this.downloadPackage(javaRelease); info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); if (process.platform === 'win32') { @@ -129936,20 +129930,49 @@ class TemurinDistribution extends JavaBase { const extractedJavaPath = await extractJdkFile(javaArchivePath, extension); const archiveName = external_fs_default().readdirSync(extractedJavaPath)[0]; const archivePath = external_path_default().join(extractedJavaPath, archiveName); + const javaHome = process.platform === 'darwin' + ? external_path_default().join(archivePath, MACOS_JAVA_CONTENT_POSTFIX) + : archivePath; + if (this.includeJmods && !external_fs_default().existsSync(external_path_default().join(javaHome, 'jmods'))) { + await this.installJmods(javaRelease.version, javaHome); + } const version = this.getToolcacheVersionName(javaRelease.version); const javaPath = await cacheDir(archivePath, this.toolcacheFolderName, version, this.architecture); return { version: javaRelease.version, path: javaPath }; } - get toolcacheFolderName() { - return super.toolcacheFolderName; - } supportsSignatureVerification() { return true; } - async getAvailableVersions() { + async downloadPackage(release) { + const archivePath = await downloadTool(release.url); + if (this.verifySignature) { + if (!release.signatureUrl) { + throw new Error(`Input 'verify-signature' is enabled, but no signature URL was found for Temurin version ${release.version}.`); + } + info(`Verifying Java package signature...`); + try { + await verifyPackageSignature(archivePath, release.signatureUrl, this.verifySignaturePublicKey ?? ADOPTIUM_PUBLIC_KEY); + } + catch (error) { + throw new Error(`Failed to verify signature for Temurin version ${release.version} from ${release.signatureUrl}: ${error.message}`, { cause: error }); + } + } + return archivePath; + } + async installJmods(version, javaHome) { + const jmodsRelease = await this.resolvePackage(version, 'jmods'); + info(`Downloading JMODs ${jmodsRelease.version} (${this.distribution}) from ${jmodsRelease.url} ...`); + let jmodsArchivePath = await this.downloadPackage(jmodsRelease); + if (process.platform === 'win32') { + jmodsArchivePath = renameWinArchive(jmodsArchivePath); + } + const extractedJmodsPath = await extractJdkFile(jmodsArchivePath, getDownloadArchiveExtension()); + const jmodsDirectory = external_path_default().join(extractedJmodsPath, external_fs_default().readdirSync(extractedJmodsPath)[0]); + external_fs_default().cpSync(jmodsDirectory, external_path_default().join(javaHome, 'jmods'), { recursive: true }); + } + async getAvailableVersions(imageType = this.includeJmods ? 'jdk' : this.packageType) { const platform = this.getPlatformOption(); const arch = this.distributionArchitecture(); - const imageType = this.packageType; const versionRange = encodeURI('[1.0,100.0]'); // retrieve all available versions const releaseType = this.stable ? 'ga' : 'ea'; if (isDebug()) { @@ -132025,6 +132048,10 @@ var JavaDistribution; JavaDistribution["OracleOpenJdk"] = "oracle-openjdk"; })(JavaDistribution || (JavaDistribution = {})); function getJavaDistribution(distributionName, installerOptions, jdkFile) { + if (installerOptions.packageType === 'jdk+jmods' && + distributionName !== JavaDistribution.Temurin) { + throw new Error("java-package 'jdk+jmods' is only supported for distribution 'temurin'."); + } switch (distributionName) { case JavaDistribution.JdkFile: return new LocalDistribution(installerOptions, jdkFile); diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 6ef042a0..af911a04 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -44,6 +44,7 @@ steps: with: distribution: 'temurin' java-version: '25' + java-package: 'jdk+jmods' # optional, includes JMOD files with JDK 24 and later - run: java --version ``` diff --git a/src/distributions/distribution-factory.ts b/src/distributions/distribution-factory.ts index d27844ee..7dcce64d 100644 --- a/src/distributions/distribution-factory.ts +++ b/src/distributions/distribution-factory.ts @@ -50,6 +50,15 @@ export function getJavaDistribution( installerOptions: JavaInstallerOptions, jdkFile?: string ): JavaBase | null { + if ( + installerOptions.packageType === 'jdk+jmods' && + distributionName !== JavaDistribution.Temurin + ) { + throw new Error( + "java-package 'jdk+jmods' is only supported for distribution 'temurin'." + ); + } + switch (distributionName) { case JavaDistribution.JdkFile: return new LocalDistribution(installerOptions, jdkFile); diff --git a/src/distributions/temurin/installer.ts b/src/distributions/temurin/installer.ts index 17956778..8064e837 100644 --- a/src/distributions/temurin/installer.ts +++ b/src/distributions/temurin/installer.ts @@ -9,6 +9,7 @@ import * as gpg from '../../gpg.js'; import {ADOPTIUM_PUBLIC_KEY} from './adoptium-key.js'; import {JavaBase} from '../base-installer.js'; import {ITemurinAvailableVersions} from './models.js'; +import {MACOS_JAVA_CONTENT_POSTFIX} from '../../constants.js'; import { JavaDownloadRelease, JavaInstallerOptions, @@ -31,11 +32,14 @@ export enum TemurinImplementation { } export class TemurinDistribution extends JavaBase { + private readonly includeJmods: boolean; + constructor( installerOptions: JavaInstallerOptions, private readonly jvmImpl: TemurinImplementation ) { super(`Temurin-${jvmImpl}`, installerOptions); + this.includeJmods = this.packageType === 'jdk+jmods'; } /** @@ -44,7 +48,17 @@ export class TemurinDistribution extends JavaBase { public async findPackageForDownload( version: string ): Promise { - const availableVersionsRaw = await this.getAvailableVersions(); + return this.resolvePackage( + version, + this.includeJmods ? 'jdk' : this.packageType + ); + } + + private async resolvePackage( + version: string, + imageType: string + ): Promise { + const availableVersionsRaw = await this.getAvailableVersions(imageType); const availableVersionsWithBinaries = availableVersionsRaw .filter(item => item.binaries.length > 0) .map(item => { @@ -83,30 +97,7 @@ export class TemurinDistribution extends JavaBase { core.info( `Downloading Java ${javaRelease.version} (${this.distribution}) from ${javaRelease.url} ...` ); - let javaArchivePath = await tc.downloadTool(javaRelease.url); - - if (this.verifySignature) { - if (!javaRelease.signatureUrl) { - throw new Error( - `Input 'verify-signature' is enabled, but no signature URL was found for Temurin version ${javaRelease.version}.` - ); - } - core.info(`Verifying Java package signature...`); - try { - await gpg.verifyPackageSignature( - javaArchivePath, - javaRelease.signatureUrl, - this.verifySignaturePublicKey ?? ADOPTIUM_PUBLIC_KEY - ); - } catch (error) { - throw new Error( - `Failed to verify signature for Temurin version ${javaRelease.version} from ${javaRelease.signatureUrl}: ${ - (error as Error).message - }`, - {cause: error} - ); - } - } + let javaArchivePath = await this.downloadPackage(javaRelease); core.info(`Extracting Java archive...`); const extension = getDownloadArchiveExtension(); @@ -117,6 +108,13 @@ export class TemurinDistribution extends JavaBase { const archiveName = fs.readdirSync(extractedJavaPath)[0]; const archivePath = path.join(extractedJavaPath, archiveName); + const javaHome = + process.platform === 'darwin' + ? path.join(archivePath, MACOS_JAVA_CONTENT_POSTFIX) + : archivePath; + if (this.includeJmods && !fs.existsSync(path.join(javaHome, 'jmods'))) { + await this.installJmods(javaRelease.version, javaHome); + } const version = this.getToolcacheVersionName(javaRelease.version); const javaPath = await tc.cacheDir( @@ -129,18 +127,64 @@ export class TemurinDistribution extends JavaBase { return {version: javaRelease.version, path: javaPath}; } - protected get toolcacheFolderName(): string { - return super.toolcacheFolderName; - } - protected supportsSignatureVerification(): boolean { return true; } - private async getAvailableVersions(): Promise { + private async downloadPackage(release: JavaDownloadRelease): Promise { + const archivePath = await tc.downloadTool(release.url); + + if (this.verifySignature) { + if (!release.signatureUrl) { + throw new Error( + `Input 'verify-signature' is enabled, but no signature URL was found for Temurin version ${release.version}.` + ); + } + core.info(`Verifying Java package signature...`); + try { + await gpg.verifyPackageSignature( + archivePath, + release.signatureUrl, + this.verifySignaturePublicKey ?? ADOPTIUM_PUBLIC_KEY + ); + } catch (error) { + throw new Error( + `Failed to verify signature for Temurin version ${release.version} from ${release.signatureUrl}: ${ + (error as Error).message + }`, + {cause: error} + ); + } + } + + return archivePath; + } + + private async installJmods(version: string, javaHome: string): Promise { + const jmodsRelease = await this.resolvePackage(version, 'jmods'); + core.info( + `Downloading JMODs ${jmodsRelease.version} (${this.distribution}) from ${jmodsRelease.url} ...` + ); + let jmodsArchivePath = await this.downloadPackage(jmodsRelease); + if (process.platform === 'win32') { + jmodsArchivePath = renameWinArchive(jmodsArchivePath); + } + const extractedJmodsPath = await extractJdkFile( + jmodsArchivePath, + getDownloadArchiveExtension() + ); + const jmodsDirectory = path.join( + extractedJmodsPath, + fs.readdirSync(extractedJmodsPath)[0] + ); + fs.cpSync(jmodsDirectory, path.join(javaHome, 'jmods'), {recursive: true}); + } + + private async getAvailableVersions( + imageType = this.includeJmods ? 'jdk' : this.packageType + ): Promise { const platform = this.getPlatformOption(); const arch = this.distributionArchitecture(); - const imageType = this.packageType; const versionRange = encodeURI('[1.0,100.0]'); // retrieve all available versions const releaseType = this.stable ? 'ga' : 'ea';