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 {
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' ;
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 ));
2022-10-10 17:47:17 -06:00
this . architecture = 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
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 {
// default mappings of config architectures to distribution architectures
// override if a distribution uses any different names; see liberica for an example
// node's os.arch() - which this defaults to - can return any of:
// 'arm', 'arm64', 'ia32', 'mips', 'mipsel', 'ppc', 'ppc64', 's390', 's390x', and 'x64'
// so we need to map these to java distribution architectures
// 'amd64' is included here too b/c it's a common alias for 'x64' people might use explicitly
switch ( this . architecture ) {
case 'amd64' :
return 'x64' ;
case 'ia32' :
return 'x86' ;
case 'arm64' :
return 'aarch64' ;
default :
return this . architecture ;
}
}
2021-04-05 13:02:27 +03:00
}