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 
This commit is contained in:
Nikolas Grottendieck 2023-09-20 00:21:55 +02:00
parent 4075bfc1b5
commit 82fce8d395
No known key found for this signature in database
GPG Key ID: 44A3563E46261FBA
3 changed files with 563 additions and 76 deletions

@ -143,7 +143,20 @@ describe('toolchains tests', () => {
</toolchain>
</toolchains>`;
const result = `<?xml version="1.0"?>
<toolchains>
<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</type>
<provides>
<version>17</version>
<vendor>Eclipse Temurin</vendor>
<id>temurin_17</id>
</provides>
<configuration>
<jdkHome>/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
@ -155,6 +168,442 @@ describe('toolchains tests', () => {
<jdkHome>/opt/jdk/sun/1.6</jdkHome>
</configuration>
</toolchain>
</toolchains>`;
fs.mkdirSync(m2Dir, {recursive: true});
fs.writeFileSync(toolchainsFile, originalFile);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
await toolchains.createToolchainsSettings({
jdkInfo,
settingsDirectory: m2Dir,
overwriteSettings: true
});
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
expect(fs.readFileSync(toolchainsFile, 'utf-8')).toEqual(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
);
expect(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
).toEqual(result);
}, 100000);
it('does not discard custom elements in existing toolchain definitions', async () => {
const jdkInfo = {
version: '17',
vendor: 'Eclipse Temurin',
id: 'temurin_17',
jdkHome: '/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64'
};
const originalFile = `<toolchains>
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>Sun</vendor>
<id>sun_1.6</id>
<custom>foo</custom>
</provides>
<configuration>
<jdkHome>/opt/jdk/sun/1.6</jdkHome>
<fooHome>/usr/local/bin/bash</fooHome>
</configuration>
</toolchain>
</toolchains>`;
const result = `<?xml version="1.0"?>
<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</type>
<provides>
<version>17</version>
<vendor>Eclipse Temurin</vendor>
<id>temurin_17</id>
</provides>
<configuration>
<jdkHome>/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>Sun</vendor>
<id>sun_1.6</id>
<custom>foo</custom>
</provides>
<configuration>
<jdkHome>/opt/jdk/sun/1.6</jdkHome>
<fooHome>/usr/local/bin/bash</fooHome>
</configuration>
</toolchain>
</toolchains>`;
fs.mkdirSync(m2Dir, {recursive: true});
fs.writeFileSync(toolchainsFile, originalFile);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
await toolchains.createToolchainsSettings({
jdkInfo,
settingsDirectory: m2Dir,
overwriteSettings: true
});
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
expect(fs.readFileSync(toolchainsFile, 'utf-8')).toEqual(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
);
expect(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
).toEqual(result);
}, 100000);
it('does not discard existing, custom toolchain definitions', async () => {
const jdkInfo = {
version: '17',
vendor: 'Eclipse Temurin',
id: 'temurin_17',
jdkHome: '/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64'
};
const originalFile = `<toolchains>
<toolchain>
<type>foo</type>
<provides>
<custom>baz</custom>
</provides>
<configuration>
<fooHome>/usr/local/bin/foo</fooHome>
</configuration>
</toolchain>
</toolchains>`;
const result = `<?xml version="1.0"?>
<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</type>
<provides>
<version>17</version>
<vendor>Eclipse Temurin</vendor>
<id>temurin_17</id>
</provides>
<configuration>
<jdkHome>/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>foo</type>
<provides>
<custom>baz</custom>
</provides>
<configuration>
<fooHome>/usr/local/bin/foo</fooHome>
</configuration>
</toolchain>
</toolchains>`;
fs.mkdirSync(m2Dir, {recursive: true});
fs.writeFileSync(toolchainsFile, originalFile);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
await toolchains.createToolchainsSettings({
jdkInfo,
settingsDirectory: m2Dir,
overwriteSettings: true
});
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
expect(fs.readFileSync(toolchainsFile, 'utf-8')).toEqual(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
);
expect(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
).toEqual(result);
}, 100000);
it('does not duplicate existing toolchain definitions', async () => {
const jdkInfo = {
version: '17',
vendor: 'Eclipse Temurin',
id: 'temurin_17',
jdkHome: '/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64'
};
const originalFile = `<toolchains>
<toolchain>
<type>jdk</type>
<provides>
<version>17</version>
<vendor>Eclipse Temurin</vendor>
<id>temurin_17</id>
</provides>
<configuration>
<jdkHome>/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64</jdkHome>
</configuration>
</toolchain>
</toolchains>`;
const result = `<?xml version="1.0"?>
<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</type>
<provides>
<version>17</version>
<vendor>Eclipse Temurin</vendor>
<id>temurin_17</id>
</provides>
<configuration>
<jdkHome>/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64</jdkHome>
</configuration>
</toolchain>
</toolchains>`;
fs.mkdirSync(m2Dir, {recursive: true});
fs.writeFileSync(toolchainsFile, originalFile);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
await toolchains.createToolchainsSettings({
jdkInfo,
settingsDirectory: m2Dir,
overwriteSettings: true
});
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
expect(fs.readFileSync(toolchainsFile, 'utf-8')).toEqual(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
);
expect(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
).toEqual(result);
}, 100000);
it('does not duplicate existing toolchain definitions if multiple exist', async () => {
const jdkInfo = {
version: '17',
vendor: 'Eclipse Temurin',
id: 'temurin_17',
jdkHome: '/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64'
};
const originalFile = `<toolchains>
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>Sun</vendor>
<id>sun_1.6</id>
</provides>
<configuration>
<jdkHome>/opt/jdk/sun/1.6</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<version>17</version>
<vendor>Eclipse Temurin</vendor>
<id>temurin_17</id>
</provides>
<configuration>
<jdkHome>/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64</jdkHome>
</configuration>
</toolchain>
</toolchains>`;
const result = `<?xml version="1.0"?>
<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</type>
<provides>
<version>17</version>
<vendor>Eclipse Temurin</vendor>
<id>temurin_17</id>
</provides>
<configuration>
<jdkHome>/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>Sun</vendor>
<id>sun_1.6</id>
</provides>
<configuration>
<jdkHome>/opt/jdk/sun/1.6</jdkHome>
</configuration>
</toolchain>
</toolchains>`;
fs.mkdirSync(m2Dir, {recursive: true});
fs.writeFileSync(toolchainsFile, originalFile);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
await toolchains.createToolchainsSettings({
jdkInfo,
settingsDirectory: m2Dir,
overwriteSettings: true
});
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
expect(fs.readFileSync(toolchainsFile, 'utf-8')).toEqual(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
);
expect(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
).toEqual(result);
}, 100000);
it('handles an empty list of existing toolchains correctly', async () => {
const jdkInfo = {
version: '17',
vendor: 'Eclipse Temurin',
id: 'temurin_17',
jdkHome: '/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64'
};
const originalFile = `<toolchains>
</toolchains>`;
const result = `<?xml version="1.0"?>
<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</type>
<provides>
<version>17</version>
<vendor>Eclipse Temurin</vendor>
<id>temurin_17</id>
</provides>
<configuration>
<jdkHome>/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64</jdkHome>
</configuration>
</toolchain>
</toolchains>`;
fs.mkdirSync(m2Dir, {recursive: true});
fs.writeFileSync(toolchainsFile, originalFile);
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
await toolchains.createToolchainsSettings({
jdkInfo,
settingsDirectory: m2Dir,
overwriteSettings: true
});
expect(fs.existsSync(m2Dir)).toBe(true);
expect(fs.existsSync(toolchainsFile)).toBe(true);
expect(fs.readFileSync(toolchainsFile, 'utf-8')).toEqual(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
);
expect(
toolchains.generateToolchainDefinition(
originalFile,
jdkInfo.version,
jdkInfo.vendor,
jdkInfo.id,
jdkInfo.jdkHome
)
).toEqual(result);
}, 100000);
it('handles an empty existing toolchains.xml correctly', async () => {
const jdkInfo = {
version: '17',
vendor: 'Eclipse Temurin',
id: 'temurin_17',
jdkHome: '/opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.1-12/x64'
};
const originalFile = ``;
const result = `<?xml version="1.0"?>
<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</type>
<provides>

63
dist/setup/index.js vendored

@ -103953,31 +103953,7 @@ function createToolchainsSettings({ jdkInfo, settingsDirectory, overwriteSetting
exports.createToolchainsSettings = createToolchainsSettings;
// only exported for testing purposes
function generateToolchainDefinition(original, version, vendor, id, jdkHome) {
let xmlObj;
if (original === null || original === void 0 ? void 0 : original.length) {
xmlObj = xmlbuilder2_1.create(original)
.root()
.ele({
toolchain: {
type: 'jdk',
provides: {
version: `${version}`,
vendor: `${vendor}`,
id: `${id}`
},
configuration: {
jdkHome: `${jdkHome}`
}
}
});
}
else
xmlObj = 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: [
let jsToolchains = [
{
type: 'jdk',
provides: {
@ -103989,10 +103965,41 @@ function generateToolchainDefinition(original, version, vendor, id, jdkHome) {
jdkHome: `${jdkHome}`
}
}
]
];
if (original === null || original === void 0 ? void 0 : 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 = xmlbuilder2_1.create(original)
.root()
.toObject();
if (jsObj.toolchains && 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);
}
});
return xmlObj.end({
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' ||
index ===
self.findIndex(t => t.type === value.type && t.provides.id === value.provides.id));
}
// TODO: technically bad because we shouldn't re-create the toolchains root node (with possibly different schema values) if it already exists, however, just overriding the toolchain array with xmlbuilder2 is … uh non-trivial
return 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: jsToolchains
}
}).end({
format: 'xml',
wellFormed: false,
headless: false,

@ -84,31 +84,7 @@ export function generateToolchainDefinition(
id: string,
jdkHome: string
) {
let xmlObj;
if (original?.length) {
xmlObj = xmlCreate(original)
.root()
.ele({
toolchain: {
type: 'jdk',
provides: {
version: `${version}`,
vendor: `${vendor}`,
id: `${id}`
},
configuration: {
jdkHome: `${jdkHome}`
}
}
});
} else
xmlObj = xmlCreate({
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: [
let jsToolchains: Toolchain[] = [
{
type: 'jdk',
provides: {
@ -120,11 +96,47 @@ export function generateToolchainDefinition(
jdkHome: `${jdkHome}`
}
}
]
];
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 && 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);
}
}
});
return xmlObj.end({
// 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' ||
index ===
self.findIndex(
t => t.type === value.type && t.provides.id === value.provides.id
)
);
}
// TODO: technically bad because we shouldn't re-create the toolchains root node (with possibly different schema values) if it already exists, however, just overriding the toolchain array with xmlbuilder2 is … uh non-trivial
return xmlCreate({
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: jsToolchains
}
}).end({
format: 'xml',
wellFormed: false,
headless: false,
@ -167,3 +179,22 @@ async function writeToolchainsFileToDisk(
flag: 'w'
});
}
interface ExtractedToolchains {
toolchains: {
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;
}