2021-04-05 13:02:27 +03:00
import * as tc from '@actions/tool-cache' ;
import * as core from '@actions/core' ;
import * as fs from 'fs' ;
import semver from 'semver' ;
import path from 'path' ;
import * as httpm from '@actions/http-client' ;
2026-07-09 15:24:02 -04:00
import {
convertVersionToSemver ,
getToolcachePath ,
isVersionSatisfies
} from '../util.js' ;
2023-03-09 14:49:35 +02:00
import {
2026-07-29 04:43:56 -04:00
ChecksumAlgorithm ,
ChecksumMetadata ,
2023-03-09 14:49:35 +02:00
JavaDownloadRelease ,
JavaInstallerOptions ,
JavaInstallerResults
2026-07-08 14:45:00 +05:30
} from './base-models.js' ;
import { MACOS_JAVA_CONTENT_POSTFIX } from '../constants.js' ;
2026-07-29 00:09:37 -04:00
import { RetryingHttpClient } from '../retrying-http-client.js' ;
2022-10-10 17:47:17 -06:00
import os from 'os' ;
2026-07-29 04:43:56 -04:00
import { expectedDigestLength , verifyChecksum } from '../checksum.js' ;
2026-07-29 15:46:29 -04:00
import { normalizeArchitecture } from './platform-types.js' ;
2021-04-05 13:02:27 +03:00
export abstract class JavaBase {
protected http : httpm.HttpClient ;
protected version : string ;
protected architecture : string ;
protected packageType : string ;
protected stable : boolean ;
2026-07-09 20:11:03 -04:00
protected latest : boolean ;
2021-04-05 13:02:27 +03:00
protected checkLatest : boolean ;
2026-07-28 14:43:47 -04:00
protected forceDownload : boolean ;
2026-07-07 18:38:57 +02:00
protected setDefault : boolean ;
2026-06-29 13:19:49 +01:00
protected verifySignature : boolean ;
protected verifySignaturePublicKey : string | undefined ;
2021-04-05 13:02:27 +03:00
2023-03-09 14:49:35 +02:00
constructor (
protected distribution : string ,
installerOptions : JavaInstallerOptions
) {
2026-07-29 00:09:37 -04:00
this . http = new RetryingHttpClient ( 'actions/setup-java' );
2021-04-05 13:02:27 +03:00
2026-07-09 20:11:03 -04:00
({
version : this.version ,
stable : this.stable ,
latest : this.latest
} = this . normalizeVersion ( installerOptions . version ));
2026-07-29 15:46:29 -04:00
this . architecture = normalizeArchitecture (
installerOptions . architecture || os . arch ()
);
2021-04-05 13:02:27 +03:00
this . packageType = installerOptions . packageType ;
this . checkLatest = installerOptions . checkLatest ;
2026-07-28 14:43:47 -04:00
this . forceDownload = installerOptions . forceDownload ?? false ;
2026-07-07 18:38:57 +02:00
this . setDefault =
installerOptions . setDefault !== undefined
? installerOptions.setDefault
: true ;
2026-06-29 13:19:49 +01:00
this . verifySignature = installerOptions . verifySignature ?? false ;
this . verifySignaturePublicKey = installerOptions . verifySignaturePublicKey ;
2021-04-05 13:02:27 +03:00
}
2023-03-09 14:49:35 +02:00
protected abstract downloadTool (
javaRelease : JavaDownloadRelease
) : Promise < JavaInstallerResults >;
protected abstract findPackageForDownload (
range : string
) : Promise < JavaDownloadRelease >;
2021-04-05 13:02:27 +03:00
2026-07-29 04:43:56 -04:00
protected async downloadAndVerify (
javaRelease : JavaDownloadRelease
) : Promise < string > {
const archivePath = await tc . downloadTool ( javaRelease . url );
const checksum = javaRelease . checksum ;
if ( ! checksum || ! checksum . value ? . trim ()) {
core . debug (
`No authoritative checksum is available for ${ this . distribution } version ${ javaRelease . version } ; skipping checksum verification.`
);
return archivePath ;
}
try {
await verifyChecksum ( archivePath , checksum , {
distribution : this.distribution ,
version : javaRelease.version
});
core . debug (
`Verified ${ checksum . algorithm } checksum for ${ this . distribution } version ${ javaRelease . version } .`
);
return archivePath ;
} catch ( error ) {
let cleanupError : unknown ;
let cleanupFailed = false ;
try {
await fs . promises . rm ( archivePath , { force : true });
} catch ( caughtCleanupError ) {
cleanupError = caughtCleanupError ;
cleanupFailed = true ;
}
if ( cleanupFailed ) {
throw new Error (
` ${ ( error as Error ). message } Failed to remove the downloaded archive after verification failure: ${ ( cleanupError as Error ). message } ` ,
{ cause : error }
);
}
throw error ;
}
}
protected async fetchChecksum (
checksumUrl : string ,
algorithm : ChecksumAlgorithm | ChecksumAlgorithm []
) : Promise < ChecksumMetadata | undefined > {
// Some vendors (e.g. JetBrains) publish a single, generically-named
// checksum sibling (`.checksum`) whose digest algorithm isn't disclosed
// by the URL and has changed across releases. Accepting a list of
// candidate algorithms lets callers pass every algorithm the vendor is
// known to use; the actual algorithm is then inferred from the length of
// the returned digest.
const algorithms = Array . isArray ( algorithm ) ? algorithm : [ algorithm ];
const algorithmLabel = algorithms . join ( ' or ' );
const response = await this . http . get ( checksumUrl );
const statusCode = response . message . statusCode ;
const source = (() => {
try {
const url = new URL ( checksumUrl );
return ` ${ url . origin }${ url . pathname } ` ;
} catch {
return 'an invalid checksum URL' ;
}
})();
if ( statusCode === httpm . HttpCodes . NotFound ) {
core . debug (
`No authoritative ${ algorithmLabel } checksum is available for ${ this . distribution } from ${ source } ; skipping checksum verification.`
);
return undefined ;
}
if ( statusCode !== httpm . HttpCodes . OK ) {
throw new Error (
`Failed to fetch the authoritative ${ algorithmLabel } checksum for ${ this . distribution } from ${ source } (HTTP ${ statusCode } ).`
);
}
const body = await response . readBody ();
const value = body . trim (). split ( /\s+/ , 1 )[ 0 ] ?? '' ;
if ( ! value ) {
throw new Error (
`Received an empty authoritative ${ algorithmLabel } checksum for ${ this . distribution } from ${ source } .`
);
}
// Prefer the strongest algorithm whose digest length matches what was
// actually returned; fall back to the first candidate (preserving prior
// behavior/error messages) when the digest doesn't match any of them.
const resolvedAlgorithm =
algorithms . find ( algo => value . length === expectedDigestLength ( algo )) ??
algorithms [ 0 ];
return { algorithm : resolvedAlgorithm , value , source : checksumUrl };
}
2021-04-05 13:02:27 +03:00
public async setupJava () : Promise < JavaInstallerResults > {
2026-06-29 13:19:49 +01:00
if ( this . verifySignature && ! this . supportsSignatureVerification ()) {
throw new Error (
`Input 'verify-signature' is not supported for distribution ' ${ this . distribution } '.`
);
}
2026-07-28 14:43:47 -04:00
let foundJava = this . forceDownload ? null : this . findInToolcache ();
2026-07-09 20:11:03 -04:00
if ( foundJava && ! this . checkLatest && ! this . latest ) {
2021-04-05 13:02:27 +03:00
core . info ( `Resolved Java ${ foundJava . version } from tool-cache` );
} else {
core . info ( 'Trying to resolve the latest version from remote' );
2026-07-29 00:09:37 -04:00
try {
const javaRelease = await this . findPackageForDownload ( this . version );
core . info ( `Resolved latest version as ${ javaRelease . version } ` );
if ( ! this . forceDownload && foundJava ? . version === javaRelease . version ) {
core . info ( `Resolved Java ${ foundJava . version } from tool-cache` );
} else {
core . info ( 'Trying to download...' );
foundJava = await this . downloadTool ( javaRelease );
core . info ( `Java ${ foundJava . version } was downloaded` );
2025-06-23 23:02:03 +05:30
}
2026-07-29 00:09:37 -04:00
} catch ( error : any ) {
this . logSetupError ( error );
throw error ;
2021-04-05 13:02:27 +03:00
}
}
2025-11-14 01:04:50 +05:30
if ( ! foundJava ) {
throw new Error ( 'Failed to resolve Java version' );
}
2021-04-05 13:02:27 +03:00
// JDK folder may contain postfix "Contents/Home" on macOS
2023-03-09 14:49:35 +02:00
const macOSPostfixPath = path . join (
foundJava . path ,
MACOS_JAVA_CONTENT_POSTFIX
);
2021-04-05 13:02:27 +03:00
if ( process . platform === 'darwin' && fs . existsSync ( macOSPostfixPath )) {
foundJava . path = macOSPostfixPath ;
}
2026-07-07 18:38:57 +02:00
if ( this . setDefault ) {
core . info ( `Setting Java ${ foundJava . version } as the default` );
this . setJavaDefault ( foundJava . version , foundJava . path );
} else {
core . info (
`Installing Java ${ foundJava . version } (not setting as default)`
);
this . setJavaEnvironment ( foundJava . version , foundJava . path );
}
2021-04-05 13:02:27 +03:00
return foundJava ;
}
2026-07-29 00:09:37 -04:00
private logSetupError ( error : any ) : void {
const httpStatusCode =
error instanceof tc . HTTPError
? error.httpStatusCode
: error instanceof httpm . HttpClientError
? error.statusCode
: undefined ;
if ( httpStatusCode ) {
if ( httpStatusCode === 403 ) {
core . error ( 'HTTP 403: Permission denied or access restricted.' );
} else if ( httpStatusCode === 429 ) {
core . warning ( 'HTTP 429: Rate limit exceeded. Please retry later.' );
} else {
core . error ( `HTTP ${ httpStatusCode } : ${ error . message } ` );
}
} else if ( error && error . errors && Array . isArray ( error . errors )) {
core . error ( `Java setup failed due to network or configuration error(s)` );
if ( error instanceof Error && error . stack ) {
core . debug ( error . stack );
}
for ( const err of error . errors ) {
const endpoint = err ? . address || err ? . hostname || '' ;
const port = err ? . port ? `: ${ err . port } ` : '' ;
const message = err ? . message || 'Aggregate error' ;
const endpointInfo = ! message . includes ( endpoint )
? ` ${ endpoint }${ port } `
: '' ;
const localInfo =
err . localAddress && err . localPort
? ` - Local ( ${ err . localAddress } : ${ err . localPort } )`
: '' ;
const logMessage = ` ${ message }${ endpointInfo }${ localInfo } ` ;
core . error ( logMessage );
core . debug ( ` ${ err . stack || err . message } ` );
Object . entries ( err ). forEach (([ key , value ]) => {
core . debug ( `" ${ key } ": ${ JSON . stringify ( value ) } ` );
});
}
} else {
const message =
error instanceof Error ? error.message : JSON.stringify ( error );
core . error ( `Java setup process failed due to: ${ message } ` );
if ( typeof error ? . code === 'string' ) {
core . debug ( error . stack );
}
const errorDetails = {
name : error.name ,
message : error.message ,
... Object . getOwnPropertyNames ( error )
. filter ( prop => ! [ 'name' , 'message' , 'stack' ]. includes ( prop ))
. reduce < {[ key : string ] : any } > (( acc , prop ) => {
acc [ prop ] = error [ prop ];
return acc ;
}, {})
};
Object . entries ( errorDetails ). forEach (([ key , value ]) => {
core . debug ( `" ${ key } ": ${ JSON . stringify ( value ) } ` );
});
}
}
2021-04-05 13:02:27 +03:00
protected get toolcacheFolderName () : string {
return `Java_ ${ this . distribution } _ ${ this . packageType } ` ;
}
2026-06-29 13:19:49 +01:00
protected supportsSignatureVerification () : boolean {
return false ;
}
2021-04-05 13:02:27 +03:00
protected getToolcacheVersionName ( version : string ) : string {
if ( ! this . stable ) {
if ( version . includes ( '+' )) {
return version . replace ( '+' , '-ea.' );
} else {
return ` ${ version } -ea` ;
}
}
// Kotlin and some Java dependencies don't work properly when Java path contains "+" sign
// so replace "/hostedtoolcache/Java/11.0.3+4/x64" to "/hostedtoolcache/Java/11.0.3-4/x64" when saves to cache
// related issue: https://github.com/actions/virtual-environments/issues/3014
return version . replace ( '+' , '-' );
}
protected findInToolcache () : JavaInstallerResults | null {
// we can't use tc.find directly because firstly, we need to filter versions by stability flag
// if *-ea is provided, take only ea versions from toolcache, otherwise - only stable versions
const availableVersions = tc
. findAllVersions ( this . toolcacheFolderName , this . architecture )
. map ( item => {
return {
version : item
. replace ( '-ea.' , '+' )
. replace ( /-ea$/ , '' )
// Kotlin and some Java dependencies don't work properly when Java path contains "+" sign
// so replace "/hostedtoolcache/Java/11.0.3-4/x64" to "/hostedtoolcache/Java/11.0.3+4/x64" when retrieves to cache
// related issue: https://github.com/actions/virtual-environments/issues/3014
. replace ( '-' , '+' ),
2023-03-09 14:49:35 +02:00
path :
getToolcachePath (
this . toolcacheFolderName ,
item ,
this . architecture
) || '' ,
2021-04-05 13:02:27 +03:00
stable : ! item . includes ( '-ea' )
};
})
. filter ( item => item . stable === this . stable );
const satisfiedVersions = availableVersions
. filter ( item => isVersionSatisfies ( this . version , item . version ))
. filter ( item => item . path )
. sort (( a , b ) => {
return - semver . compareBuild ( a . version , b . version );
});
if ( ! satisfiedVersions || satisfiedVersions . length === 0 ) {
return null ;
}
return {
version : satisfiedVersions [ 0 ]. version ,
path : satisfiedVersions [ 0 ]. path
};
}
protected normalizeVersion ( version : string ) {
let stable = true ;
2026-07-09 20:11:03 -04:00
const latest = false ;
// Support the `latest` alias (case-insensitive), which floats to the newest
// available stable/GA release. It is translated to the SemVer wildcard `x`
// so the existing "newest satisfying version wins" resolution applies.
const normalized = version . trim (). toLowerCase ();
if ( normalized === 'latest' ) {
return {
version : 'x' ,
stable : true ,
latest : true
};
}
// Reject `latest` combined with any qualifier (e.g. `latest-ea`). Such inputs
// would otherwise have their `-ea` suffix stripped and fall through to the
// generic SemVer check, which fails with a confusing "'latest' is not valid
// SemVer" message even though `latest` is a supported value. Fail early with a
// targeted explanation instead.
if ( normalized . startsWith ( 'latest' )) {
throw new Error (
`The 'latest' alias resolves stable (GA) releases only and cannot be combined with '-ea' or other qualifiers (received ' ${ version } '). Use 'latest' on its own, or specify a concrete version.`
);
}
2021-04-05 13:02:27 +03:00
if ( version . endsWith ( '-ea' )) {
version = version . replace ( /-ea$/ , '' );
stable = false ;
} else if ( version . includes ( '-ea.' )) {
// transform '11.0.3-ea.2' -> '11.0.3+2'
version = version . replace ( '-ea.' , '+' );
stable = false ;
}
2026-07-09 15:24:02 -04:00
// Java uses a versioning scheme (JEP 322) that can contain more numeric
// fields than SemVer allows, e.g. '18.0.1.1' or '11.0.9.1'. Convert such
// exact versions to SemVer build notation ('18.0.1+1') so they are
// accepted. Ranges and versions that already carry build metadata are
// left untouched.
if ( /^\d+(\.\d+){3,}$/ . test ( version )) {
version = convertVersionToSemver ( version );
}
2021-04-05 13:02:27 +03:00
if ( ! semver . validRange ( version )) {
throw new Error (
`The string ' ${ version } ' is not valid SemVer notation for a Java version. Please check README file for code snippets and more detailed information`
);
}
return {
version ,
2026-07-09 20:11:03 -04:00
stable ,
latest
2021-04-05 13:02:27 +03:00
};
}
2026-04-13 23:14:45 +05:30
protected createVersionNotFoundError (
versionOrRange : string ,
availableVersions? : string [],
additionalContext? : string
) : Error {
const parts = [
`No matching version found for SemVer ' ${ versionOrRange } '.` ,
`Distribution: ${ this . distribution } ` ,
`Package type: ${ this . packageType } ` ,
`Architecture: ${ this . architecture } `
];
// Add additional context if provided (e.g., platform/OS info)
if ( additionalContext ) {
parts . push ( additionalContext );
}
if ( availableVersions && availableVersions . length > 0 ) {
const maxVersionsToShow = core . isDebug () ? availableVersions.length : 50 ;
const versionsToShow = availableVersions . slice ( 0 , maxVersionsToShow );
const truncated = availableVersions . length > maxVersionsToShow ;
parts . push (
`Available versions: ${ versionsToShow . join ( ', ' ) }${ truncated ? ', ...' : '' } `
);
if ( truncated ) {
parts . push (
`(showing first ${ maxVersionsToShow } of ${ availableVersions . length } versions, enable debug mode to see all)`
);
}
}
2026-06-12 16:30:59 +01:00
const error = new Error ( parts . join ( '\n' ));
error . name = 'VersionNotFoundError' ;
return error ;
2026-04-13 23:14:45 +05:30
}
2021-04-05 13:02:27 +03:00
protected setJavaDefault ( version : string , toolPath : string ) {
core . exportVariable ( 'JAVA_HOME' , toolPath );
core . addPath ( path . join ( toolPath , 'bin' ));
2026-07-07 18:38:57 +02:00
this . setJavaEnvironment ( version , toolPath );
}
protected setJavaEnvironment ( version : string , toolPath : string ) {
const majorVersion = version . split ( '.' )[ 0 ];
2021-04-05 13:02:27 +03:00
core . setOutput ( 'distribution' , this . distribution );
core . setOutput ( 'path' , toolPath );
core . setOutput ( 'version' , version );
2023-03-09 14:49:35 +02:00
core . exportVariable (
`JAVA_HOME_ ${ majorVersion } _ ${ this . architecture . toUpperCase () } ` ,
toolPath
);
2021-04-05 13:02:27 +03:00
}
2022-10-10 17:47:17 -06:00
protected distributionArchitecture () : string {
2026-07-29 15:46:29 -04:00
return this . architecture ;
2022-10-10 17:47:17 -06:00
}
2021-04-05 13:02:27 +03:00
}