From 7f1a95c821836ec7dd4bad105c1700fdbf3eca6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:46:08 +0000 Subject: [PATCH] Add Temurin JMOD installation support --- .../distributors/temurin-installer.test.ts | 108 +++++++++++++++++- action.yml | 4 + docs/advanced-usage.md | 1 + src/constants.ts | 1 + src/distributions/base-models.ts | 1 + src/distributions/temurin/installer.ts | 106 ++++++++++++----- src/setup-java.ts | 6 + 7 files changed, 197 insertions(+), 30 deletions(-) diff --git a/__tests__/distributors/temurin-installer.test.ts b/__tests__/distributors/temurin-installer.test.ts index dd2d73a5..33f5fe04 100644 --- a/__tests__/distributors/temurin-installer.test.ts +++ b/__tests__/distributors/temurin-installer.test.ts @@ -169,6 +169,28 @@ describe('getAvailableVersions', () => { } ); + it('requests the JMOD image type', async () => { + const distribution = new TemurinDistribution( + { + version: '25', + architecture: 'x64', + packageType: 'jdk', + jmod: true, + 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'; @@ -228,13 +250,20 @@ describe('getAvailableVersions', () => { }); it.each([ - [TemurinImplementation.Hotspot, 'jdk', 'Java_Temurin-Hotspot_jdk'], - [TemurinImplementation.Hotspot, 'jre', 'Java_Temurin-Hotspot_jre'] + [TemurinImplementation.Hotspot, 'jdk', false, 'Java_Temurin-Hotspot_jdk'], + [TemurinImplementation.Hotspot, 'jre', false, 'Java_Temurin-Hotspot_jre'], + [ + TemurinImplementation.Hotspot, + 'jdk', + true, + 'Java_Temurin-Hotspot_jdk_jmods' + ] ])( 'find right toolchain folder', ( impl: TemurinImplementationType, packageType: string, + jmod: boolean, expected: string ) => { const distribution = new TemurinDistribution( @@ -242,6 +271,7 @@ describe('getAvailableVersions', () => { version: '8', architecture: 'x64', packageType: packageType, + jmod, checkLatest: false }, impl @@ -252,6 +282,24 @@ describe('getAvailableVersions', () => { } ); + it('rejects JMODs with a non-JDK package', () => { + expect( + () => + new TemurinDistribution( + { + version: '25', + architecture: 'x64', + packageType: 'jre', + jmod: true, + checkLatest: false + }, + TemurinImplementation.Hotspot + ) + ).toThrow( + "Input 'jmod' is only supported with Temurin java-package 'jdk'." + ); + }); + it.each([ ['amd64', 'x64'], ['arm64', 'aarch64'] @@ -386,6 +434,7 @@ describe('downloadTool', () => { let spyCacheDir: any; let spyReadDirSync: any; let spyRenameWinArchive: any; + let spyCopySync: any; beforeEach(() => { spyDownloadTool = tc.downloadTool as jest.Mock; @@ -400,6 +449,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 +484,59 @@ 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', + jmod: true, + 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( + '/tmp/extracted-jmods/jdk-25-jmods', + '/tmp/extracted/jdk-25/jmods', + {recursive: true} + ); + expect(spyCacheDir).toHaveBeenCalledWith( + '/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..b4b1a819 100644 --- a/action.yml +++ b/action.yml @@ -16,6 +16,10 @@ inputs: description: 'The package type (jdk, jre, jdk+fx, jre+fx, jdk+crac, jre+crac)' required: false default: 'jdk' + jmod: + description: 'Set this option to true to include JMOD files with Temurin JDK 24 and later' + required: false + default: false architecture: description: "The architecture of the package (defaults to the action runner's architecture)" required: false diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 6ef042a0..29608f94 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -44,6 +44,7 @@ steps: with: distribution: 'temurin' java-version: '25' + jmod: true # optional, includes JMOD files with JDK 24 and later - run: java --version ``` diff --git a/src/constants.ts b/src/constants.ts index e1688db4..8dfb88a4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,7 @@ export const INPUT_JAVA_VERSION = 'java-version'; export const INPUT_JAVA_VERSION_FILE = 'java-version-file'; export const INPUT_ARCHITECTURE = 'architecture'; export const INPUT_JAVA_PACKAGE = 'java-package'; +export const INPUT_JMOD = 'jmod'; export const INPUT_DISTRIBUTION = 'distribution'; export const INPUT_JDK_FILE = 'jdk-file'; export const INPUT_JDK_FILE_DEPRECATED = 'jdkFile'; diff --git a/src/distributions/base-models.ts b/src/distributions/base-models.ts index 3bfe4bb9..911aab30 100644 --- a/src/distributions/base-models.ts +++ b/src/distributions/base-models.ts @@ -2,6 +2,7 @@ export interface JavaInstallerOptions { version: string; architecture: string; packageType: string; + jmod?: boolean; checkLatest: boolean; forceDownload?: boolean; setDefault?: boolean; diff --git a/src/distributions/temurin/installer.ts b/src/distributions/temurin/installer.ts index 17956778..280adb25 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,19 @@ export enum TemurinImplementation { } export class TemurinDistribution extends JavaBase { + private readonly jmod: boolean; + constructor( installerOptions: JavaInstallerOptions, private readonly jvmImpl: TemurinImplementation ) { super(`Temurin-${jvmImpl}`, installerOptions); + this.jmod = installerOptions.jmod ?? false; + if (this.jmod && this.packageType !== 'jdk') { + throw new Error( + `Input 'jmod' is only supported with Temurin java-package 'jdk'.` + ); + } } /** @@ -44,7 +53,14 @@ export class TemurinDistribution extends JavaBase { public async findPackageForDownload( version: string ): Promise { - const availableVersionsRaw = await this.getAvailableVersions(); + return this.resolvePackage(version, 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 +99,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 +110,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.jmod && !fs.existsSync(path.join(javaHome, 'jmods'))) { + await this.installJmods(javaRelease.version, javaHome); + } const version = this.getToolcacheVersionName(javaRelease.version); const javaPath = await tc.cacheDir( @@ -130,17 +130,67 @@ export class TemurinDistribution extends JavaBase { } protected get toolcacheFolderName(): string { - return super.toolcacheFolderName; + return `${super.toolcacheFolderName}${this.jmod ? '_jmods' : ''}`; } 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.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'; diff --git a/src/setup-java.ts b/src/setup-java.ts index cc0f9370..42858acd 100644 --- a/src/setup-java.ts +++ b/src/setup-java.ts @@ -23,6 +23,7 @@ async function run() { const versionFile = core.getInput(constants.INPUT_JAVA_VERSION_FILE); const architecture = core.getInput(constants.INPUT_ARCHITECTURE); const packageType = core.getInput(constants.INPUT_JAVA_PACKAGE); + const jmod = getBooleanInput(constants.INPUT_JMOD, false); const jdkFile = getJdkFileInput(); const cache = core.getInput(constants.INPUT_CACHE); const cacheDependencyPath = core.getInput( @@ -86,6 +87,7 @@ async function run() { const installerInputsOptions: installerInputsOptions = { architecture, packageType, + jmod, checkLatest, forceDownload, setDefault, @@ -106,6 +108,7 @@ async function run() { const installerInputsOptions: installerInputsOptions = { architecture, packageType, + jmod, checkLatest, forceDownload, setDefault, @@ -164,6 +167,7 @@ async function installVersion( jdkFile, architecture, packageType, + jmod, checkLatest, forceDownload, setDefault, @@ -175,6 +179,7 @@ async function installVersion( const installerOptions: JavaInstallerOptions = { architecture, packageType, + jmod, checkLatest, forceDownload, setDefault, @@ -219,6 +224,7 @@ async function installVersion( interface installerInputsOptions { architecture: string; packageType: string; + jmod: boolean; checkLatest: boolean; forceDownload: boolean; setDefault: boolean;