fix: Maven Toolchains grows unexpectedly (#534)

* fix: Maven Toolchains grows unexpectedly

On self-hosted runners toolchains.xml may survive multiple runs and unexpectedly
grow as a result of the toolchains setup simply appending the JDK definition
even if one with the same `type` and `provides.id` already exists.

Restructuring the parsing step and filtering the potentially existing list of
toolchain definitions prevents this and also fixes toolchain.xml files that
already contain duplicates.

Fixes #530

* fix: guard toolchain dedup and preserve existing root attributes

Address reviewer feedback on the Maven toolchains dedup logic:

- Treat jdk toolchains without a string `provides.id` as non-deduplicatable
  and use optional access when comparing ids, so partially-formed or
  nonstandard toolchains.xml files no longer crash setup.
- Preserve the existing `<toolchains>` root attributes (xmlns,
  schemaLocation, …) when present, falling back to the 1.1.0 defaults only
  for attributes the existing file is missing. This avoids silently
  rewriting user-managed metadata or changing the effective namespace.

Adds tests covering custom root attributes and id-less jdk toolchains, and
rebuilds dist/.

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

---------

Co-authored-by: Bruno Borges <brborges@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Nikolas Grottendieck
2026-07-07 06:01:31 +02:00
committed by GitHub
parent 0765b158bf
commit 77ee41d00e
3 changed files with 757 additions and 74 deletions
+54 -34
View File
@@ -81506,46 +81506,66 @@ function createToolchainsSettings({ jdkInfo, settingsDirectory, overwriteSetting
exports.createToolchainsSettings = createToolchainsSettings;
// only exported for testing purposes
function generateToolchainDefinition(original, version, vendor, id, jdkHome) {
let xmlObj;
let jsToolchains = [
{
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 = {
'@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 === null || original === void 0 ? void 0 : original.length) {
xmlObj = (0, xmlbuilder2_1.create)(original)
// 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 = (0, xmlbuilder2_1.create)(original)
.root()
.ele({
toolchain: {
type: 'jdk',
provides: {
version: `${version}`,
vendor: `${vendor}`,
id: `${id}`
},
configuration: {
jdkHome: `${jdkHome}`
.toObject();
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('@')));
// fall back to the defaults only for attributes the existing file is missing
rootAttributes = Object.assign(Object.assign({}, 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) => {
var _a;
// ensure non-jdk toolchains are kept in the results, we must not touch them because they belong to the user
return 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 ((_a = value.provides) === null || _a === void 0 ? void 0 : _a.id) !== 'string' ||
index ===
self.findIndex(t => { var _a, _b; return t.type === value.type && ((_a = t.provides) === null || _a === void 0 ? void 0 : _a.id) === ((_b = value.provides) === null || _b === void 0 ? void 0 : _b.id); });
});
}
else
xmlObj = (0, xmlbuilder2_1.create)({
toolchains: {
'@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',
toolchain: [
{
type: 'jdk',
provides: {
version: `${version}`,
vendor: `${vendor}`,
id: `${id}`
},
configuration: {
jdkHome: `${jdkHome}`
}
}
]
}
});
return xmlObj.end({
return (0, xmlbuilder2_1.create)({
toolchains: Object.assign(Object.assign({}, rootAttributes), { toolchain: jsToolchains })
}).end({
format: 'xml',
wellFormed: false,
headless: false,