mirror of
https://github.com/actions/setup-java.git
synced 2026-07-29 01:53:06 +03:00
* Preserve Maven toolchains across repeated setup-java runs (#1099) Backport of #1111 to releases/v5. Toolchain generation was gated behind the `overwrite-settings` input, which is documented to control only regeneration of `settings.xml`. Because `generateToolchainDefinition` already performs a non-destructive merge (existing JDK, custom, and user-managed toolchains are preserved, and only an entry with the same `type` + `provides.id` is replaced), skipping the write when `overwrite-settings: false` caused later setup-java executions to drop toolchain entries registered by earlier runs. Decouple toolchains generation from `overwrite-settings`: the toolchains file is now always written, so consecutive runs accumulate every JDK. `settings.xml` behavior (auth.ts) is unchanged. - src/toolchains.ts: drop overwriteSettings from configureToolchains / createToolchainsSettings / writeToolchainsFileToDisk; always write. - __tests__/toolchains.test.ts: update call sites, rewrite the "does not overwrite" test to assert non-destructive extension, and add a regression test for consecutive configureToolchains executions. - docs/advanced-usage.md: clarify merge is non-destructive and independent of overwrite-settings. - dist/setup/index.js: rebuilt. Fixes #1099 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
204 lines
6.4 KiB
TypeScript
204 lines
6.4 KiB
TypeScript
import * as fs from 'fs';
|
||
import * as os from 'os';
|
||
import * as path from 'path';
|
||
import * as core from '@actions/core';
|
||
import * as io from '@actions/io';
|
||
import * as constants from './constants';
|
||
|
||
import {create as xmlCreate} from 'xmlbuilder2';
|
||
|
||
interface JdkInfo {
|
||
version: string;
|
||
vendor: string;
|
||
id: string;
|
||
jdkHome: string;
|
||
}
|
||
|
||
export async function configureToolchains(
|
||
version: string,
|
||
distributionName: string,
|
||
jdkHome: string,
|
||
toolchainId?: string
|
||
) {
|
||
const vendor =
|
||
core.getInput(constants.INPUT_MVN_TOOLCHAIN_VENDOR) || distributionName;
|
||
const id = toolchainId || `${vendor}_${version}`;
|
||
const settingsDirectory =
|
||
core.getInput(constants.INPUT_SETTINGS_PATH) ||
|
||
path.join(os.homedir(), constants.M2_DIR);
|
||
|
||
await createToolchainsSettings({
|
||
jdkInfo: {
|
||
version,
|
||
vendor,
|
||
id,
|
||
jdkHome
|
||
},
|
||
settingsDirectory
|
||
});
|
||
}
|
||
|
||
export async function createToolchainsSettings({
|
||
jdkInfo,
|
||
settingsDirectory
|
||
}: {
|
||
jdkInfo: JdkInfo;
|
||
settingsDirectory: string;
|
||
}) {
|
||
core.info(
|
||
`Creating ${constants.MVN_TOOLCHAINS_FILE} for JDK version ${jdkInfo.version} from ${jdkInfo.vendor}`
|
||
);
|
||
// when an alternate m2 location is specified use only that location (no .m2 directory)
|
||
// otherwise use the home/.m2/ path
|
||
await io.mkdirP(settingsDirectory);
|
||
const originalToolchains =
|
||
await readExistingToolchainsFile(settingsDirectory);
|
||
const updatedToolchains = generateToolchainDefinition(
|
||
originalToolchains,
|
||
jdkInfo.version,
|
||
jdkInfo.vendor,
|
||
jdkInfo.id,
|
||
jdkInfo.jdkHome
|
||
);
|
||
await writeToolchainsFileToDisk(settingsDirectory, updatedToolchains);
|
||
}
|
||
|
||
// only exported for testing purposes
|
||
export function generateToolchainDefinition(
|
||
original: string,
|
||
version: string,
|
||
vendor: string,
|
||
id: string,
|
||
jdkHome: string
|
||
) {
|
||
let jsToolchains: Toolchain[] = [
|
||
{
|
||
type: 'jdk',
|
||
provides: {
|
||
version: `${version}`,
|
||
vendor: `${vendor}`,
|
||
id: `${id}`
|
||
},
|
||
configuration: {
|
||
jdkHome: `${jdkHome}`
|
||
}
|
||
}
|
||
];
|
||
// default root attributes, used when the existing file does not declare its own
|
||
let rootAttributes: Record<string, string> = {
|
||
'@xmlns': 'http://maven.apache.org/TOOLCHAINS/1.1.0',
|
||
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||
'@xsi:schemaLocation':
|
||
'http://maven.apache.org/TOOLCHAINS/1.1.0 https://maven.apache.org/xsd/toolchains-1.1.0.xsd'
|
||
};
|
||
if (original?.length) {
|
||
// convert existing toolchains into TS native objects for better handling
|
||
// xmlbuilder2 will convert the document into a `{toolchains: { toolchain: [] | {} }}` structure
|
||
// instead of the desired `toolchains: [{}]` one or simply `[{}]`
|
||
const jsObj = xmlCreate(original)
|
||
.root()
|
||
.toObject() as unknown as ExtractedToolchains;
|
||
if (jsObj.toolchains) {
|
||
// preserve the existing root attributes (xmlns, schemaLocation, …) so we don't
|
||
// silently rewrite user-managed metadata or change the effective XML namespace;
|
||
// xmlbuilder2 exposes attributes as `@`-prefixed keys on the element object
|
||
const existingAttributes = Object.fromEntries(
|
||
Object.entries(jsObj.toolchains).filter(([key]) => key.startsWith('@'))
|
||
) as Record<string, string>;
|
||
// fall back to the defaults only for attributes the existing file is missing
|
||
rootAttributes = {...rootAttributes, ...existingAttributes};
|
||
|
||
if (jsObj.toolchains.toolchain) {
|
||
// in case only a single child exists xmlbuilder2 will not create an array and using verbose = true equally doesn't work here
|
||
// See https://oozcitak.github.io/xmlbuilder2/serialization.html#js-object-and-map-serializers for details
|
||
if (Array.isArray(jsObj.toolchains.toolchain)) {
|
||
jsToolchains.push(...jsObj.toolchains.toolchain);
|
||
} else {
|
||
jsToolchains.push(jsObj.toolchains.toolchain);
|
||
}
|
||
}
|
||
}
|
||
|
||
// remove potential duplicates based on type & id (which should be a unique combination);
|
||
// self.findIndex will only return the first occurrence, ensuring duplicates are skipped
|
||
jsToolchains = jsToolchains.filter(
|
||
(value, index, self) =>
|
||
// ensure non-jdk toolchains are kept in the results, we must not touch them because they belong to the user
|
||
value.type !== 'jdk' ||
|
||
// keep toolchains that lack a usable string id (e.g. partially-formed user files);
|
||
// we cannot safely deduplicate them and must not crash while reading them
|
||
typeof value.provides?.id !== 'string' ||
|
||
index ===
|
||
self.findIndex(
|
||
t => t.type === value.type && t.provides?.id === value.provides?.id
|
||
)
|
||
);
|
||
}
|
||
|
||
return xmlCreate({
|
||
toolchains: {
|
||
...rootAttributes,
|
||
toolchain: jsToolchains
|
||
}
|
||
}).end({
|
||
format: 'xml',
|
||
wellFormed: false,
|
||
headless: false,
|
||
prettyPrint: true,
|
||
width: 80
|
||
});
|
||
}
|
||
|
||
async function readExistingToolchainsFile(directory: string) {
|
||
const location = path.join(directory, constants.MVN_TOOLCHAINS_FILE);
|
||
if (fs.existsSync(location)) {
|
||
return fs.readFileSync(location, {
|
||
encoding: 'utf-8',
|
||
flag: 'r'
|
||
});
|
||
}
|
||
return '';
|
||
}
|
||
|
||
async function writeToolchainsFileToDisk(directory: string, settings: string) {
|
||
const location = path.join(directory, constants.MVN_TOOLCHAINS_FILE);
|
||
const settingsExists = fs.existsSync(location);
|
||
// The toolchains file is produced by a non-destructive merge (existing JDK,
|
||
// custom, and non-jdk toolchains are preserved – see generateToolchainDefinition),
|
||
// so it is always safe to write it. Unlike settings.xml, it is therefore not
|
||
// gated behind the `overwrite-settings` input; that would prevent subsequent
|
||
// setup-java runs from registering additional JDKs and silently drop the
|
||
// toolchain entries created by earlier runs.
|
||
if (settingsExists) {
|
||
core.info(`Updating existing file ${location}`);
|
||
} else {
|
||
core.info(`Writing to ${location}`);
|
||
}
|
||
|
||
return fs.writeFileSync(location, settings, {
|
||
encoding: 'utf-8',
|
||
flag: 'w'
|
||
});
|
||
}
|
||
|
||
interface ExtractedToolchains {
|
||
toolchains: {
|
||
// root attributes such as xmlns / schemaLocation are exposed as `@`-prefixed keys
|
||
[attribute: `@${string}`]: string;
|
||
toolchain?: Toolchain[] | Toolchain;
|
||
};
|
||
}
|
||
|
||
// Toolchain type definition according to Maven Toolchains XSD 1.1.0
|
||
interface Toolchain {
|
||
type: string;
|
||
provides:
|
||
| {
|
||
version: string;
|
||
vendor: string;
|
||
id: string;
|
||
}
|
||
| any;
|
||
configuration: any;
|
||
}
|