diff --git a/docs/usage/java.md b/docs/usage/java.md index 8ef03e87429..161c58aeaf3 100644 --- a/docs/usage/java.md +++ b/docs/usage/java.md @@ -5,7 +5,7 @@ description: Java versions support in Renovate # Java Dependency Updates -Renovate can update Gradle and Maven dependencies. +Renovate can update Gradle, Maven, and Ant dependencies. This includes libraries and plugins as well as the Gradle Wrapper. ## LTS releases diff --git a/lib/modules/manager/ant/extract.spec.ts b/lib/modules/manager/ant/extract.spec.ts index 67f0329e178..b97e3d54728 100644 --- a/lib/modules/manager/ant/extract.spec.ts +++ b/lib/modules/manager/ant/extract.spec.ts @@ -25,7 +25,6 @@ describe('modules/manager/ant/extract', () => { depName: 'junit:junit', currentValue: '4.13.2', depType: 'test', - registryUrls: [], }), ], }); @@ -783,7 +782,6 @@ describe('modules/manager/ant/extract', () => { depName: 'junit:junit', currentValue: '4.13.2', depType: 'compile', - registryUrls: [], }), ], }, @@ -948,6 +946,248 @@ describe('modules/manager/ant/extract', () => { ]); }); + it('collects registry URLs from remoteRepository elements', async () => { + fs.readLocalFile.mockImplementation((fileName: string) => { + const files: Record = { + 'build.xml': codeBlock` + + + + + + + `, + }; + return Promise.resolve(files[fileName] ?? null); + }); + + const result = await extractAllPackageFiles({}, ['build.xml']); + + expect(result).toEqual([ + { + packageFile: 'build.xml', + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + registryUrls: ['https://repo.example.com/maven2'], + }), + ], + }, + ]); + }); + + it('passes registry URLs to coords-style dependencies', async () => { + fs.readLocalFile.mockImplementation((fileName: string) => { + const files: Record = { + 'build.xml': codeBlock` + + + + + + + `, + }; + return Promise.resolve(files[fileName] ?? null); + }); + + const result = await extractAllPackageFiles({}, ['build.xml']); + + expect(result).toEqual([ + { + packageFile: 'build.xml', + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + registryUrls: ['https://repo.example.com/maven2'], + }), + ], + }, + ]); + }); + + it('collects registry URLs from settingsFile attribute', async () => { + fs.readLocalFile.mockImplementation((fileName: string) => { + const files: Record = { + 'build.xml': codeBlock` + + + + + + `, + 'build/settings.xml': codeBlock` + + + + https://artifactory.example.com/maven + + + + `, + }; + return Promise.resolve(files[fileName] ?? null); + }); + + const result = await extractAllPackageFiles({}, ['build.xml']); + + expect(result).toEqual([ + { + packageFile: 'build.xml', + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + registryUrls: ['https://artifactory.example.com/maven'], + }), + ], + }, + ]); + }); + + it('merges registries from settingsFile and remoteRepository', async () => { + fs.readLocalFile.mockImplementation((fileName: string) => { + const files: Record = { + 'build.xml': codeBlock` + + + + + + + `, + 'build/settings.xml': codeBlock` + + + + https://artifactory.example.com/maven + + + + `, + }; + return Promise.resolve(files[fileName] ?? null); + }); + + const result = await extractAllPackageFiles({}, ['build.xml']); + + expect(result).toEqual([ + { + packageFile: 'build.xml', + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + registryUrls: [ + 'https://artifactory.example.com/maven', + 'https://repo.example.com/maven2', + ], + }), + ], + }, + ]); + }); + + it('handles absolute settingsFile path', async () => { + fs.readLocalFile.mockImplementation((fileName: string) => { + const files: Record = { + 'build.xml': codeBlock` + + + + + + `, + '/etc/maven/settings.xml': codeBlock` + + + + https://internal.example.com/maven + + + + `, + }; + return Promise.resolve(files[fileName] ?? null); + }); + + const result = await extractAllPackageFiles({}, ['build.xml']); + + expect(result).toEqual([ + { + packageFile: 'build.xml', + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + registryUrls: ['https://internal.example.com/maven'], + }), + ], + }, + ]); + }); + + it('logs debug when settingsFile cannot be read', async () => { + fs.readLocalFile.mockImplementation((fileName: string) => { + const files: Record = { + 'build.xml': codeBlock` + + + + + + `, + }; + return Promise.resolve(files[fileName] ?? null); + }); + + const result = await extractAllPackageFiles({}, ['build.xml']); + + expect(result).toEqual([ + { + packageFile: 'build.xml', + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + }), + ], + }, + ]); + }); + + it('does not pass registries to dependencies outside the block', async () => { + fs.readLocalFile.mockImplementation((fileName: string) => { + const files: Record = { + 'build.xml': codeBlock` + + + + + + + + + + `, + }; + return Promise.resolve(files[fileName] ?? null); + }); + + const result = await extractAllPackageFiles({}, ['build.xml']); + + expect(result).toEqual([ + { + packageFile: 'build.xml', + deps: [ + expect.objectContaining({ + depName: 'junit:junit', + registryUrls: ['https://repo.example.com/maven2'], + }), + expect.objectContaining({ + depName: 'org.slf4j:slf4j-api', + }), + ], + }, + ]); + }); + it('handles chain referencing undefined property', async () => { fs.readLocalFile.mockResolvedValue(codeBlock` diff --git a/lib/modules/manager/ant/extract.ts b/lib/modules/manager/ant/extract.ts index 6bc781e614e..0d27ef93eb7 100644 --- a/lib/modules/manager/ant/extract.ts +++ b/lib/modules/manager/ant/extract.ts @@ -4,6 +4,7 @@ import { XmlDocument } from 'xmldoc'; import { logger } from '../../../logger/index.ts'; import { readLocalFile } from '../../../util/fs/index.ts'; import { MavenDatasource } from '../../datasource/maven/index.ts'; +import { extractRegistries } from '../maven/extract.ts'; import { isXmlElement } from '../nuget/util.ts'; import type { ExtractConfig, @@ -75,10 +76,45 @@ interface RawDep { depPackageFile: string; } +async function collectRegistryUrls( + node: XmlElement, + baseDir: string, +): Promise { + const urls: string[] = []; + + // Read registry URLs from settingsFile attribute + const settingsFile = node.attr.settingsFile; + if (settingsFile) { + const settingsPath = settingsFile.startsWith('/') + ? settingsFile + : upath.join(baseDir, settingsFile); + const settingsContent = await readLocalFile(settingsPath, 'utf8'); + if (settingsContent) { + urls.push(...extractRegistries(settingsContent)); + } else { + logger.debug(`ant manager: could not read settings file ${settingsPath}`); + } + } + + // Collect inline elements + for (const child of node.children) { + if ( + isXmlElement(child) && + child.name === 'remoteRepository' && + child.attr.url + ) { + urls.push(child.attr.url); + } + } + + return [...new Set(urls)]; +} + function collectCoordsDependency( node: XmlElement, packageFile: string, content: string, + registryUrls: string[], ): RawDep | null { const coordsStr = node.attr.coords; @@ -92,7 +128,7 @@ function collectCoordsDependency( depName: `${parsed.groupId}:${parsed.artifactId}`, currentValue: parsed.rawVersion, depType: getDependencyType(parsed.scope ?? node.attr.scope), - registryUrls: [], + ...(registryUrls?.length && { registryUrls }), }; // Position at the version substring within the coords attribute value @@ -107,9 +143,10 @@ function collectDependency( node: XmlElement, packageFile: string, content: string, + registryUrls: string[] = [], ): RawDep | null { if (node.attr.coords) { - return collectCoordsDependency(node, packageFile, content); + return collectCoordsDependency(node, packageFile, content, registryUrls); } const { groupId, artifactId, version, scope } = node.attr; @@ -123,7 +160,7 @@ function collectDependency( depName: `${groupId}:${artifactId}`, currentValue: version, depType: getDependencyType(scope), - registryUrls: [], + ...(registryUrls?.length && { registryUrls }), }; dep.fileReplacePosition = findAttrValuePosition(content, node, 'version'); @@ -188,6 +225,7 @@ async function walkNodeInOrder( visitedFiles: Set, allProps: Record, allRawDeps: RawDep[], + registryUrls: string[] = [], ): Promise { const baseDir = upath.dirname(packageFile); @@ -230,11 +268,20 @@ async function walkNodeInOrder( ); await walkXmlFile(importedFile, visitedFiles, allProps, allRawDeps); } else if (child.name === 'dependency') { - const rawDep = collectDependency(child, packageFile, content); + const rawDep = collectDependency( + child, + packageFile, + content, + registryUrls, + ); if (rawDep) { allRawDeps.push(rawDep); } } else { + // Collect registry URLs from settingsFile and remoteRepository + const childRegistries = await collectRegistryUrls(child, baseDir); + const mergedUrls = + childRegistries.length > 0 ? childRegistries : registryUrls; await walkNodeInOrder( child, packageFile, @@ -242,6 +289,7 @@ async function walkNodeInOrder( visitedFiles, allProps, allRawDeps, + mergedUrls, ); } } diff --git a/lib/modules/manager/ant/readme.md b/lib/modules/manager/ant/readme.md index 09fbd672b58..711fa584ba9 100644 --- a/lib/modules/manager/ant/readme.md +++ b/lib/modules/manager/ant/readme.md @@ -1,2 +1,28 @@ -Extracts Apache Ant dependencies from `build.xml` files that use the `maven-resolver-ant-tasks` library. +Extracts Apache Ant dependencies from `build.xml` files that use the `maven-resolver-ant-tasks` or `maven-ant-tasks` library. Dependencies are looked up using the Maven datasource. + +### Supported syntax + +Renovate extracts dependencies from `` elements in two formats: + +- Separate attributes: `groupId`, `artifactId`, `version`, and optional `scope` +- Coords attribute: `coords="group:artifact:version"` or `coords="group:artifact:version:scope"` + +### Property resolution + +Version values can reference Ant properties defined via `` or loaded from external `.properties` files via ``. +Ant's first-definition-wins semantics are respected. + +### File traversal + +Renovate follows `` elements to extract dependencies from imported build files. +Properties defined before an `` are available in the imported file. + +### Registry URLs + +Renovate discovers Maven registry URLs from: + +- `settingsFile` attribute on `` elements (parsed as a Maven `settings.xml`) +- Inline `` elements within dependency blocks + +Discovered registries are scoped to their dependency block.