diff --git a/README.md b/README.md index 06a71287..40b6af49 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. For Eclipse Temurin 24 and later, `jdk+jmods` includes the separately packaged JMOD files. Default value: `jdk`. + - `java-package`: The packaging variant of the chosen distribution. Possible values across all distributions are `jdk`, `jre`, `jdk+fx`, `jre+fx`, `jdk+crac`, `jre+crac`, `jdk+jmods`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, and `jre+ft`. Supported values vary by distribution; see the [package compatibility table](docs/advanced-usage.md#package-compatibility). 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 index 915e7a78..7c04da52 100644 --- a/__tests__/distributors/distribution-factory.test.ts +++ b/__tests__/distributors/distribution-factory.test.ts @@ -1,5 +1,16 @@ import {getJavaDistribution} from '../../src/distributions/distribution-factory.js'; import {RetryingHttpClient} from '../../src/retrying-http-client.js'; +import { + JAVA_PACKAGE_CAPABILITIES, + JavaDistribution +} from '../../src/distributions/package-types.js'; + +const installerOptions = (packageType: string, version = '25') => ({ + version, + architecture: 'x64', + packageType, + checkLatest: false +}); describe('getJavaDistribution', () => { it.each([ @@ -33,16 +44,71 @@ describe('getJavaDistribution', () => { expect(distribution!['http']).toBeInstanceOf(RetryingHttpClient); }); + it.each( + Object.entries(JAVA_PACKAGE_CAPABILITIES).flatMap( + ([distributionName, packageTypes]) => + packageTypes.map(packageType => [distributionName, packageType]) + ) + )('accepts %s with java-package %s', (distributionName, packageType) => { + expect( + getJavaDistribution( + distributionName, + installerOptions(packageType as string) + ) + ).not.toBeNull(); + }); + + it.each(Object.entries(JAVA_PACKAGE_CAPABILITIES))( + 'rejects unsupported java-package values for %s', + (distributionName, packageTypes) => { + expect(() => + getJavaDistribution(distributionName, installerOptions('jdk+typo')) + ).toThrow( + `Java package 'jdk+typo' is not supported for distribution '${distributionName}'. Supported package types: ${packageTypes.join(', ')}.` + ); + } + ); + it("rejects java-package 'jdk+jmods' for non-Temurin distributions", () => { expect(() => - getJavaDistribution('zulu', { - version: '25', - architecture: 'x64', - packageType: 'jdk+jmods', - checkLatest: false - }) + getJavaDistribution('zulu', installerOptions('jdk+jmods')) ).toThrow( - "java-package 'jdk+jmods' is only supported for distribution 'temurin'." + "Java package 'jdk+jmods' is not supported for distribution 'zulu'. Supported package types: jdk, jre, jdk+fx, jre+fx, jdk+crac, jre+crac." ); }); + + it.each(['8', '23.x', '23.0.1.1', '<24'])( + "rejects Temurin java-package 'jdk+jmods' for version %s", + version => { + expect(() => + getJavaDistribution( + JavaDistribution.Temurin, + installerOptions('jdk+jmods', version) + ) + ).toThrow( + `Java package 'jdk+jmods' is not supported for distribution 'temurin'. Supported package types: jdk, jre, jdk+jmods. Package 'jdk+jmods' requires Java 24 or later; requested version '${version}'.` + ); + } + ); + + it.each(['24', '24.0.1.1', '25-ea', '>=21', 'latest'])( + "accepts Temurin java-package 'jdk+jmods' for version %s", + version => { + expect( + getJavaDistribution( + JavaDistribution.Temurin, + installerOptions('jdk+jmods', version) + ) + ).not.toBeNull(); + } + ); + + it('preserves unsupported distribution handling', () => { + expect( + getJavaDistribution( + 'not-a-distribution', + installerOptions('not-a-package') + ) + ).toBeNull(); + }); }); diff --git a/__tests__/java-package-contract.test.ts b/__tests__/java-package-contract.test.ts new file mode 100644 index 00000000..ea320ef9 --- /dev/null +++ b/__tests__/java-package-contract.test.ts @@ -0,0 +1,82 @@ +import fs from 'fs'; +import path from 'path'; +import { + JAVA_PACKAGE_CAPABILITIES, + JavaDistribution +} from '../src/distributions/package-types.js'; + +const repositoryRoot = process.cwd(); +const readRepositoryFile = (filePath: string) => + fs.readFileSync(path.join(repositoryRoot, filePath), 'utf8'); +const allPackageTypes = [ + ...new Set(Object.values(JAVA_PACKAGE_CAPABILITIES).flat()) +]; + +describe('java-package published contract', () => { + it.each(['action.yml', 'README.md'])( + 'documents every supported package type in %s', + filePath => { + const content = readRepositoryFile(filePath); + const contractLine = + filePath === 'action.yml' + ? content.match(/ {2}java-package:\n(?: {4}.+\n)+/)?.[0] + : content + .split('\n') + .find(line => line.includes('- `java-package`:')); + + expect(contractLine).toBeDefined(); + for (const packageType of allPackageTypes) { + expect(contractLine).toContain(`\`${packageType}\``); + } + } + ); + + it.each(Object.entries(JAVA_PACKAGE_CAPABILITIES))( + 'keeps the advanced compatibility table aligned for %s', + (distributionName, packageTypes) => { + const advancedUsage = readRepositoryFile('docs/advanced-usage.md'); + const compatibilityTable = advancedUsage.slice( + advancedUsage.indexOf('### Package compatibility') + ); + const compatibilityRow = compatibilityTable + .split('\n') + .find( + line => + line.startsWith('|') && line.includes(`\`${distributionName}\``) + ); + + expect(compatibilityRow).toBeDefined(); + for (const packageType of packageTypes) { + expect(compatibilityRow).toContain(`\`${packageType}\``); + } + } + ); + + it('only exercises supported distribution/package combinations in E2E', () => { + const workflow = readRepositoryFile('.github/workflows/e2e-versions.yml'); + const defaultMatrix = workflow.match( + /distribution:\s*\n\s*\[([^\]]+)\]\s*\n\s*java-package:\s*\['([^']+)'\]/ + ); + expect(defaultMatrix).not.toBeNull(); + + const defaultDistributions = [ + ...defaultMatrix![1].matchAll(/'([^']+)'/g) + ].map(match => match[1]); + const defaultPackage = defaultMatrix![2]; + for (const distributionName of defaultDistributions) { + expect(supportedPackagesFor(distributionName)).toContain(defaultPackage); + } + + const includedPackages = workflow.matchAll( + /- distribution: '([^']+)'\s*\n\s*java-package: ([^\s]+)/g + ); + for (const match of includedPackages) { + const [, distributionName, packageType] = match; + expect(supportedPackagesFor(distributionName)).toContain(packageType); + } + }); +}); + +function supportedPackagesFor(distributionName: string): readonly string[] { + return JAVA_PACKAGE_CAPABILITIES[distributionName as JavaDistribution] ?? []; +} diff --git a/action.yml b/action.yml index 264238d3..d980a9dd 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, jdk+jmods)' + description: 'The package type (`jdk`, `jre`, `jdk+fx`, `jre+fx`, `jdk+crac`, `jre+crac`, `jdk+jmods`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, or `jre+ft`). Supported values vary by distribution.' required: false default: 'jdk' architecture: diff --git a/dist/setup/index.js b/dist/setup/index.js index 7f15ff1a..bd129e37 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -132121,21 +132121,7 @@ class OpenJdkDistribution extends JavaBase { } } -;// CONCATENATED MODULE: ./src/distributions/distribution-factory.ts - - - - - - - - - - - - - - +;// CONCATENATED MODULE: ./src/distributions/package-types.ts var JavaDistribution; @@ -132160,11 +132146,108 @@ var JavaDistribution; JavaDistribution["Kona"] = "kona"; 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'."); +const JAVA_PACKAGE_CAPABILITIES = { + [JavaDistribution.Adopt]: ['jdk', 'jre'], + [JavaDistribution.AdoptHotspot]: ['jdk', 'jre'], + [JavaDistribution.AdoptOpenJ9]: ['jdk', 'jre'], + [JavaDistribution.Temurin]: ['jdk', 'jre', 'jdk+jmods'], + [JavaDistribution.Zulu]: [ + 'jdk', + 'jre', + 'jdk+fx', + 'jre+fx', + 'jdk+crac', + 'jre+crac' + ], + [JavaDistribution.Liberica]: ['jdk', 'jre', 'jdk+fx', 'jre+fx'], + [JavaDistribution.LibericaNik]: ['jdk', 'jdk+fx'], + [JavaDistribution.JdkFile]: ['jdk'], + [JavaDistribution.Microsoft]: ['jdk'], + [JavaDistribution.Semeru]: ['jdk', 'jre'], + [JavaDistribution.Corretto]: ['jdk', 'jre'], + [JavaDistribution.Oracle]: ['jdk'], + [JavaDistribution.Dragonwell]: ['jdk'], + [JavaDistribution.SapMachine]: ['jdk', 'jre'], + [JavaDistribution.GraalVM]: ['jdk'], + [JavaDistribution.GraalVMCommunity]: ['jdk'], + [JavaDistribution.JetBrains]: [ + 'jdk', + 'jre', + 'jdk+jcef', + 'jre+jcef', + 'jdk+ft', + 'jre+ft' + ], + [JavaDistribution.Kona]: ['jdk'], + [JavaDistribution.OracleOpenJdk]: ['jdk'] +}; +function validateJavaPackage(distributionName, packageType, version) { + if (!isJavaDistribution(distributionName)) { + return; } + const supportedPackages = JAVA_PACKAGE_CAPABILITIES[distributionName]; + if (!supportedPackages.includes(packageType)) { + throw createUnsupportedPackageError(distributionName, packageType, supportedPackages); + } + if (distributionName === JavaDistribution.Temurin && + packageType === 'jdk+jmods' && + !canResolveTemurinJmods(version)) { + throw createUnsupportedPackageError(distributionName, packageType, supportedPackages, `Package 'jdk+jmods' requires Java 24 or later; requested version '${version}'.`); + } +} +function isJavaDistribution(value) { + return Object.prototype.hasOwnProperty.call(JAVA_PACKAGE_CAPABILITIES, value); +} +function canResolveTemurinJmods(version) { + const normalizedVersion = version.trim().toLowerCase(); + if (normalizedVersion === 'latest') { + return true; + } + let normalizedRange = normalizedVersion + .replace(/-ea$/, '') + .replace('-ea.', '+'); + if (/^\d+(\.\d+){3,}$/.test(normalizedRange)) { + normalizedRange = convertVersionToSemver(normalizedRange); + } + if (!semver_default().validRange(normalizedRange)) { + // JavaBase owns general version validation and its targeted error messages. + return true; + } + return semver_default().intersects(normalizedRange, '>=24.0.0', { + includePrerelease: true + }); +} +function createUnsupportedPackageError(distributionName, packageType, supportedPackages, detail) { + const message = [ + `Java package '${packageType}' is not supported for distribution '${distributionName}'.`, + `Supported package types: ${supportedPackages.join(', ')}.` + ]; + if (detail) { + message.push(detail); + } + return new Error(message.join(' ')); +} + +;// CONCATENATED MODULE: ./src/distributions/distribution-factory.ts + + + + + + + + + + + + + + + + + +function getJavaDistribution(distributionName, installerOptions, jdkFile) { + validateJavaPackage(distributionName, installerOptions.packageType, installerOptions.version); switch (distributionName) { case JavaDistribution.JdkFile: return new LocalDistribution(installerOptions, jdkFile); diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 0b93e5a8..9c4ef5de 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -321,12 +321,10 @@ The package types have these meanings: | `graalvm-community` | `jdk` | Stable GraalVM Community releases for JDK 17 and later only. | | `jetbrains` | `jdk`, `jre`, `jdk+jcef`, `jre+jcef`, `jdk+ft`, `jre+ft` | JetBrains publishes selected LTS-based releases rather than every OpenJDK patch. JDK/JRE and JCEF bundles start with the Java 11 release family; FreeType bundles start with Java 17. Exact package, LTS family, patch, OS, and architecture availability is determined from release assets. | | `kona` | `jdk` | Stable Java 8, 11, 17, 21, and 25 releases only. | -| `jdkfile` | `jdk` (recommended) | The package contents and version are supplied by `jdk-file`; `setup-java` does not validate them. `java-package` only separates the local archive's tool-cache entry, so use `jdk` unless separate cache namespaces are required. | +| `jdkfile` | `jdk` | The package contents and version are supplied by `jdk-file`; `setup-java` validates the package type but does not inspect the archive contents. | -Values outside this table are unsupported even when a distribution forwards the -value to its vendor API instead of rejecting it immediately. In that case, the -action normally fails with a version-not-found error because no matching -artifact exists. +Values outside this table are unsupported. The action rejects them before +checking the tool cache or requesting a vendor catalog. ```yaml steps: diff --git a/src/distributions/distribution-factory.ts b/src/distributions/distribution-factory.ts index 7dcce64d..7cc578ca 100644 --- a/src/distributions/distribution-factory.ts +++ b/src/distributions/distribution-factory.ts @@ -22,42 +22,18 @@ import { import {JetBrainsDistribution} from './jetbrains/installer.js'; import {KonaDistribution} from './kona/installer.js'; import {OpenJdkDistribution} from './openjdk/installer.js'; - -enum JavaDistribution { - Adopt = 'adopt', - AdoptHotspot = 'adopt-hotspot', - AdoptOpenJ9 = 'adopt-openj9', - Temurin = 'temurin', - Zulu = 'zulu', - Liberica = 'liberica', - LibericaNik = 'liberica-nik', - JdkFile = 'jdkfile', - Microsoft = 'microsoft', - Semeru = 'semeru', - Corretto = 'corretto', - Oracle = 'oracle', - Dragonwell = 'dragonwell', - SapMachine = 'sapmachine', - GraalVM = 'graalvm', - GraalVMCommunity = 'graalvm-community', - JetBrains = 'jetbrains', - Kona = 'kona', - OracleOpenJdk = 'oracle-openjdk' -} +import {JavaDistribution, validateJavaPackage} from './package-types.js'; export function getJavaDistribution( distributionName: string, 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'." - ); - } + validateJavaPackage( + distributionName, + installerOptions.packageType, + installerOptions.version + ); switch (distributionName) { case JavaDistribution.JdkFile: diff --git a/src/distributions/package-types.ts b/src/distributions/package-types.ts new file mode 100644 index 00000000..1c79830c --- /dev/null +++ b/src/distributions/package-types.ts @@ -0,0 +1,135 @@ +import semver from 'semver'; +import {convertVersionToSemver} from '../util.js'; + +export enum JavaDistribution { + Adopt = 'adopt', + AdoptHotspot = 'adopt-hotspot', + AdoptOpenJ9 = 'adopt-openj9', + Temurin = 'temurin', + Zulu = 'zulu', + Liberica = 'liberica', + LibericaNik = 'liberica-nik', + JdkFile = 'jdkfile', + Microsoft = 'microsoft', + Semeru = 'semeru', + Corretto = 'corretto', + Oracle = 'oracle', + Dragonwell = 'dragonwell', + SapMachine = 'sapmachine', + GraalVM = 'graalvm', + GraalVMCommunity = 'graalvm-community', + JetBrains = 'jetbrains', + Kona = 'kona', + OracleOpenJdk = 'oracle-openjdk' +} + +export const JAVA_PACKAGE_CAPABILITIES = { + [JavaDistribution.Adopt]: ['jdk', 'jre'], + [JavaDistribution.AdoptHotspot]: ['jdk', 'jre'], + [JavaDistribution.AdoptOpenJ9]: ['jdk', 'jre'], + [JavaDistribution.Temurin]: ['jdk', 'jre', 'jdk+jmods'], + [JavaDistribution.Zulu]: [ + 'jdk', + 'jre', + 'jdk+fx', + 'jre+fx', + 'jdk+crac', + 'jre+crac' + ], + [JavaDistribution.Liberica]: ['jdk', 'jre', 'jdk+fx', 'jre+fx'], + [JavaDistribution.LibericaNik]: ['jdk', 'jdk+fx'], + [JavaDistribution.JdkFile]: ['jdk'], + [JavaDistribution.Microsoft]: ['jdk'], + [JavaDistribution.Semeru]: ['jdk', 'jre'], + [JavaDistribution.Corretto]: ['jdk', 'jre'], + [JavaDistribution.Oracle]: ['jdk'], + [JavaDistribution.Dragonwell]: ['jdk'], + [JavaDistribution.SapMachine]: ['jdk', 'jre'], + [JavaDistribution.GraalVM]: ['jdk'], + [JavaDistribution.GraalVMCommunity]: ['jdk'], + [JavaDistribution.JetBrains]: [ + 'jdk', + 'jre', + 'jdk+jcef', + 'jre+jcef', + 'jdk+ft', + 'jre+ft' + ], + [JavaDistribution.Kona]: ['jdk'], + [JavaDistribution.OracleOpenJdk]: ['jdk'] +} as const satisfies Record; + +export function validateJavaPackage( + distributionName: string, + packageType: string, + version: string +): void { + if (!isJavaDistribution(distributionName)) { + return; + } + + const supportedPackages: readonly string[] = + JAVA_PACKAGE_CAPABILITIES[distributionName]; + if (!supportedPackages.includes(packageType)) { + throw createUnsupportedPackageError( + distributionName, + packageType, + supportedPackages + ); + } + + if ( + distributionName === JavaDistribution.Temurin && + packageType === 'jdk+jmods' && + !canResolveTemurinJmods(version) + ) { + throw createUnsupportedPackageError( + distributionName, + packageType, + supportedPackages, + `Package 'jdk+jmods' requires Java 24 or later; requested version '${version}'.` + ); + } +} + +function isJavaDistribution(value: string): value is JavaDistribution { + return Object.prototype.hasOwnProperty.call(JAVA_PACKAGE_CAPABILITIES, value); +} + +function canResolveTemurinJmods(version: string): boolean { + const normalizedVersion = version.trim().toLowerCase(); + if (normalizedVersion === 'latest') { + return true; + } + + let normalizedRange = normalizedVersion + .replace(/-ea$/, '') + .replace('-ea.', '+'); + if (/^\d+(\.\d+){3,}$/.test(normalizedRange)) { + normalizedRange = convertVersionToSemver(normalizedRange); + } + if (!semver.validRange(normalizedRange)) { + // JavaBase owns general version validation and its targeted error messages. + return true; + } + + return semver.intersects(normalizedRange, '>=24.0.0', { + includePrerelease: true + }); +} + +function createUnsupportedPackageError( + distributionName: JavaDistribution, + packageType: string, + supportedPackages: readonly string[], + detail?: string +): Error { + const message = [ + `Java package '${packageType}' is not supported for distribution '${distributionName}'.`, + `Supported package types: ${supportedPackages.join(', ')}.` + ]; + if (detail) { + message.push(detail); + } + return new Error(message.join(' ')); +}