setup-java/src/setup-java.ts
Bruno Borges 5827477733
Optimize Maven configuration warm path (#1182)
* Optimize Maven configuration warm path

Avoid eager Maven XML initialization on warm JDK runs by using deterministic serializers for new Maven settings/toolchains files, lazy-loading xmlbuilder2 for existing toolchains merges, and deferring Maven configuration modules until after Java setup.

Add targeted tests for XML escaping, lazy xmlbuilder2 loading, concurrent Maven configuration, and a manual benchmark workflow for warm-path validation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b

* Address Maven optimization PR feedback

Make the toolchain XML generator consistently async, remove redundant Maven configuration await handling, and reuse the existing XML test helper.

Configure CodeQL to skip generated dist output so newly split vendored chunks do not report duplicate generated-code alerts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b

* Apply rubber duck review suggestions

Document XML attribute escaping, simplify Maven configuration awaiting, and add a regression test that feeds fast-path toolchains output into the merge path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b

* Delete .github/codeql/codeql-config.yml

* Update codeql-analysis.yml

* Replace xmlbuilder2 in Maven toolchain merge

Use fast-xml-parser for existing toolchains.xml parsing and serialize merged Maven toolchains deterministically. This removes the bundled xmlbuilder2 DOM/XML builder chunk from dist while preserving merge behavior for custom attributes, custom toolchains, partial entries, duplicate filtering, and escaping.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b

* Move Maven benchmark out of setup-java

Remove the Maven warm-path benchmark workflow and helper script from setup-java. Benchmark coverage is being moved to actions/setup-java-benchmarks so this action repository only carries the runtime optimization, tests, and generated distribution artifacts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b

---------

Copilot-Session: 4dc58426-5e20-44cb-af16-8da0965fac3b
2026-07-30 15:09:35 -04:00

303 lines
8.7 KiB
TypeScript

import fs from 'fs';
import * as core from '@actions/core';
import {getBooleanInput, getVersionFromFileContent} from './util.js';
import * as constants from './constants.js';
import * as path from 'path';
import {fileURLToPath} from 'url';
import {getJavaDistribution} from './distributions/distribution-factory.js';
import {JavaInstallerOptions} from './distributions/base-models.js';
import {configureProblemMatcher} from './problem-matcher.js';
import {validateToolchainIds} from './toolchain-ids.js';
export async function run() {
const versions = core.getMultilineInput(constants.INPUT_JAVA_VERSION);
let distributionName = core.getInput(constants.INPUT_DISTRIBUTION);
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 jdkFile = getJdkFileInput();
const cache = core.getInput(constants.INPUT_CACHE);
const cacheDependencyPath = core.getInput(
constants.INPUT_CACHE_DEPENDENCY_PATH
);
const cachePath = core.getMultilineInput(constants.INPUT_CACHE_PATH);
const checkLatest = getBooleanInput(constants.INPUT_CHECK_LATEST, false);
const forceDownload = getBooleanInput(constants.INPUT_FORCE_DOWNLOAD, false);
const setDefault = getBooleanInput(constants.INPUT_SET_DEFAULT, true);
const verifySignature = getBooleanInput(
constants.INPUT_VERIFY_SIGNATURE,
false
);
const verifySignaturePublicKey =
core.getInput(constants.INPUT_VERIFY_SIGNATURE_PUBLIC_KEY) || undefined;
const toolchainIds = core.getMultilineInput(constants.INPUT_MVN_TOOLCHAIN_ID);
let actionError: Error | undefined;
let cacheRestore: Promise<void> | undefined;
const toolchainConfigurations: ToolchainConfiguration[] = [];
try {
core.startGroup('Installed distributions');
if (!versions.length && !versionFile) {
throw new Error('java-version or java-version-file input expected');
}
validateToolchainIds(versions, versionFile, toolchainIds);
if (!versions.length) {
core.debug(
'java-version input is empty, looking for java-version-file input'
);
const content = fs.readFileSync(versionFile).toString().trim();
const versionInfo = getVersionFromFileContent(
content,
distributionName,
versionFile
);
core.debug(`Parsed version from file '${versionInfo?.version}'`);
if (!versionInfo) {
throw new Error(
`No supported version was found in file ${versionFile}`
);
}
// Use distribution from file if available, otherwise use the input
if (versionInfo.distribution) {
core.info(
`Using distribution '${versionInfo.distribution}' from ${versionFile}`
);
distributionName = versionInfo.distribution;
} else if (!distributionName) {
throw new Error(
'distribution input is required when not specified in the version file'
);
}
const installerInputsOptions: installerInputsOptions = {
architecture,
packageType,
checkLatest,
forceDownload,
setDefault,
verifySignature,
verifySignaturePublicKey,
distributionName,
jdkFile,
toolchainIds
};
cacheRestore = cache
? startCacheRestore(cache, cacheDependencyPath, cachePath)
: undefined;
toolchainConfigurations.push(
await installVersion(versionInfo.version, installerInputsOptions)
);
} else {
// When using java-version input, distribution is still required
if (!distributionName) {
throw new Error('distribution input is required');
}
const installerInputsOptions: installerInputsOptions = {
architecture,
packageType,
checkLatest,
forceDownload,
setDefault,
verifySignature,
verifySignaturePublicKey,
distributionName,
jdkFile,
toolchainIds
};
cacheRestore = cache
? startCacheRestore(cache, cacheDependencyPath, cachePath)
: undefined;
for (const [index, version] of versions.entries()) {
toolchainConfigurations.push(
await installVersion(version, installerInputsOptions, index)
);
}
}
core.endGroup();
const matchersPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'.github'
);
configureProblemMatcher(path.join(matchersPath, 'java.json'));
await configureMaven(toolchainConfigurations);
} catch (error) {
actionError = error as Error;
}
if (cacheRestore) {
try {
await cacheRestore;
} catch (error) {
if (!actionError) {
actionError = error as Error;
}
}
}
if (actionError) {
core.setFailed(actionError.message);
}
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
run();
} else {
// https://nodejs.org/api/modules.html#modules_accessing_the_main_module
core.info('the script is loaded as a module, so skipping the execution');
}
function getJdkFileInput(): string {
const jdkFile = core.getInput(constants.INPUT_JDK_FILE);
const deprecatedJdkFile = core.getInput(constants.INPUT_JDK_FILE_DEPRECATED);
if (deprecatedJdkFile) {
core.warning(
`The '${constants.INPUT_JDK_FILE_DEPRECATED}' input is deprecated and may be removed in a future release. Please use '${constants.INPUT_JDK_FILE}' instead.`
);
}
return jdkFile || deprecatedJdkFile;
}
async function installVersion(
version: string,
options: installerInputsOptions,
toolchainId = 0
): Promise<ToolchainConfiguration> {
const {
distributionName,
jdkFile,
architecture,
packageType,
checkLatest,
forceDownload,
setDefault,
verifySignature,
verifySignaturePublicKey,
toolchainIds
} = options;
const installerOptions: JavaInstallerOptions = {
architecture,
packageType,
checkLatest,
forceDownload,
setDefault,
verifySignature,
verifySignaturePublicKey,
version
};
const distribution = await getJavaDistribution(
distributionName,
installerOptions,
jdkFile
);
if (!distribution) {
throw new Error(
`No supported distribution was found for input ${distributionName}`
);
}
const result = await distribution.setupJava();
// When the `latest` alias is used, the literal input isn't a real version, so
// pass the resolved version to the toolchains configuration instead.
const isLatest = version.trim().toLowerCase() === 'latest';
const toolchainVersion = isLatest ? result.version : version;
core.info('');
core.info('Java configuration:');
core.info(` Distribution: ${distributionName}`);
core.info(` Version: ${result.version}`);
core.info(` Path: ${result.path}`);
core.info('');
return {
version: toolchainVersion,
distributionName,
path: result.path,
toolchainId: toolchainIds[toolchainId]
};
}
interface installerInputsOptions {
architecture: string;
packageType: string;
checkLatest: boolean;
forceDownload: boolean;
setDefault: boolean;
verifySignature: boolean;
verifySignaturePublicKey: string | undefined;
distributionName: string;
jdkFile: string;
toolchainIds: Array<string>;
}
interface ToolchainConfiguration {
version: string;
distributionName: string;
path: string;
toolchainId?: string;
}
async function configureMaven(
toolchainConfigurations: ToolchainConfiguration[]
): Promise<void> {
const authentication = import('./auth.js').then(auth =>
auth.configureAuthentication()
);
const toolchains = configureInstalledToolchains(toolchainConfigurations);
const results = await Promise.allSettled([authentication, toolchains]);
const failure = results.find(
(result): result is PromiseRejectedResult => result.status === 'rejected'
);
if (failure) {
throw failure.reason;
}
const {configureMavenArgs} = await import('./maven-args.js');
configureMavenArgs();
}
async function configureInstalledToolchains(
toolchainConfigurations: ToolchainConfiguration[]
): Promise<void> {
const toolchains = await import('./toolchains.js');
for (const configuration of toolchainConfigurations) {
await toolchains.configureToolchains(
configuration.version,
configuration.distributionName,
configuration.path,
configuration.toolchainId
);
}
}
async function startCacheRestore(
cache: string,
cacheDependencyPath: string,
cachePath: string[]
): Promise<void> {
const {isCacheFeatureAvailable} = await import('./cache-feature.js');
if (!isCacheFeatureAvailable()) {
return;
}
const {restore} = await import('./cache.js');
await restore(cache, cacheDependencyPath, cachePath);
}