Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion docs/usage/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -205,3 +205,34 @@ To avoid JSON-in-JSON wrapping, which can cause problems, encode the JSON servic
]
}
```

## Ant
Comment thread
RahulGautamSingh marked this conversation as resolved.
Outdated

Renovate can extract dependencies from Apache Ant `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 `<dependency>` 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 `<property name="..." value="..."/>` or loaded from external `.properties` files via `<property file="..."/>`.
Ant's first-definition-wins semantics are respected.

### File traversal

Renovate follows `<import file="..."/>` elements to extract dependencies from imported build files.
Properties defined before an `<import>` are available in the imported file.

### Registry URLs

Renovate discovers Maven registry URLs from:

- `settingsFile` attribute on `<artifact:dependencies>` elements (parsed as a Maven `settings.xml`)
- Inline `<remoteRepository url="..." />` elements within dependency blocks

Discovered registries are scoped to their dependency block.
214 changes: 214 additions & 0 deletions lib/modules/manager/ant/extract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,220 @@ describe('modules/manager/ant/extract', () => {
]);
});

it('collects registry URLs from remoteRepository elements', async () => {
fs.readLocalFile.mockImplementation((fileName: string) => {
const files: Record<string, string> = {
'build.xml': codeBlock`
<project>
<artifact:dependencies>
<remoteRepository url="https://repo.example.com/maven2" />
<dependency groupId="junit" artifactId="junit" version="4.13.2" />
</artifact:dependencies>
</project>
`,
};
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<string, string> = {
'build.xml': codeBlock`
<project>
<artifact:dependencies settingsFile="build/settings.xml">
<dependency groupId="junit" artifactId="junit" version="4.13.2" />
</artifact:dependencies>
</project>
`,
'build/settings.xml': codeBlock`
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
<mirrors>
<mirror>
<url>https://artifactory.example.com/maven</url>
</mirror>
</mirrors>
</settings>
`,
};
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<string, string> = {
'build.xml': codeBlock`
<project>
<artifact:dependencies settingsFile="build/settings.xml">
<remoteRepository url="https://repo.example.com/maven2" />
<dependency groupId="junit" artifactId="junit" version="4.13.2" />
</artifact:dependencies>
</project>
`,
'build/settings.xml': codeBlock`
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
<mirrors>
<mirror>
<url>https://artifactory.example.com/maven</url>
</mirror>
</mirrors>
</settings>
`,
};
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<string, string> = {
'build.xml': codeBlock`
<project>
<artifact:dependencies settingsFile="/etc/maven/settings.xml">
<dependency groupId="junit" artifactId="junit" version="4.13.2" />
</artifact:dependencies>
</project>
`,
'/etc/maven/settings.xml': codeBlock`
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
<mirrors>
<mirror>
<url>https://internal.example.com/maven</url>
</mirror>
</mirrors>
</settings>
`,
};
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<string, string> = {
'build.xml': codeBlock`
<project>
<artifact:dependencies settingsFile="missing/settings.xml">
<dependency groupId="junit" artifactId="junit" version="4.13.2" />
</artifact:dependencies>
</project>
`,
};
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: [],
}),
],
},
]);
});

it('does not pass registries to dependencies outside the block', async () => {
fs.readLocalFile.mockImplementation((fileName: string) => {
const files: Record<string, string> = {
'build.xml': codeBlock`
<project>
<artifact:dependencies>
<remoteRepository url="https://repo.example.com/maven2" />
<dependency groupId="junit" artifactId="junit" version="4.13.2" />
</artifact:dependencies>
<artifact:dependencies>
<dependency groupId="org.slf4j" artifactId="slf4j-api" version="1.7.36" />
</artifact:dependencies>
</project>
`,
};
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',
registryUrls: [],
}),
],
},
]);
});

it('handles chain referencing undefined property', async () => {
fs.readLocalFile.mockResolvedValue(codeBlock`
<project>
Expand Down
56 changes: 52 additions & 4 deletions lib/modules/manager/ant/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -75,10 +76,45 @@ interface RawDep {
depPackageFile: string;
}

async function collectRegistryUrls(
node: XmlElement,
baseDir: string,
): Promise<string[]> {
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 <remoteRepository url="..." /> 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;

Expand All @@ -92,7 +128,7 @@ function collectCoordsDependency(
depName: `${parsed.groupId}:${parsed.artifactId}`,
currentValue: parsed.rawVersion,
depType: getDependencyType(parsed.scope ?? node.attr.scope),
registryUrls: [],
registryUrls,
};

// Position at the version substring within the coords attribute value
Expand All @@ -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;
Expand All @@ -123,7 +160,7 @@ function collectDependency(
depName: `${groupId}:${artifactId}`,
currentValue: version,
depType: getDependencyType(scope),
registryUrls: [],
registryUrls,
Comment thread
RahulGautamSingh marked this conversation as resolved.
Outdated
};

dep.fileReplacePosition = findAttrValuePosition(content, node, 'version');
Expand Down Expand Up @@ -188,6 +225,7 @@ async function walkNodeInOrder(
visitedFiles: Set<string>,
allProps: Record<string, AntProp>,
allRawDeps: RawDep[],
registryUrls: string[] = [],
): Promise<void> {
const baseDir = upath.dirname(packageFile);

Expand Down Expand Up @@ -230,18 +268,28 @@ 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,
content,
visitedFiles,
allProps,
allRawDeps,
mergedUrls,
);
}
}
Expand Down
Loading