diff --git a/docs/usage/java.md b/docs/usage/java.md index e58dbe408117b24f98c7ba75bbeffa9e234dbef2..aa8c51072dc721abbcb7389c89591def53acf834 100644 --- a/docs/usage/java.md +++ b/docs/usage/java.md @@ -16,12 +16,18 @@ Renovate detects versions that are specified in a string `'group:artifact:versio Renovate can update `build.gradle`/`build.gradle.kts` files in the root of the repository. It also updates any `*.gradle`/`*.gradle.kts` files in a subdirectory as multi-project configurations. +Renovate also tries to find updates for dependencies whose version is defined in a `*.properties` file, and scans for `*.versions.toml` files and for `*.toml` files inside the `gradle` folder to keep [catalogs](https://docs.gradle.org/current/userguide/platforms.html) up to date. Renovate does not support: - Projects which do not have either a `build.gradle` or `build.gradle.kts` in the repository root - Android projects that require extra configuration to run (e.g. setting the Android SDK) -- Gradle versions prior to version 5.0. +- Gradle versions older than version 5.0 +- Catalogs defined inside a `build.gradle` or `build.gradle.kts` file rather than in TOML +- Catalogs with version ranges +- Catalogs using the `required`, `strictly`, `preferred`, `reject`, and `rejectAll` version declarations +- Catalogs with custom names that do not end in `.toml` +- Catalogs outside the `gradle` folder whose names do not end in `.versions.toml` ## Gradle Wrapper diff --git a/lib/manager/gradle/index.ts b/lib/manager/gradle/index.ts index 086aab095fdef340987f7f217c1b2b72d0c27eb4..087bf18a9a37ea208adf47f8ca4964189cd613f7 100644 --- a/lib/manager/gradle/index.ts +++ b/lib/manager/gradle/index.ts @@ -29,7 +29,12 @@ export function updateDependency( export const language = LANGUAGE_JAVA; export const defaultConfig = { - fileMatch: ['\\.gradle(\\.kts)?$', '(^|/)gradle.properties$'], + fileMatch: [ + '\\.gradle(\\.kts)?$', + '(^|/)gradle.properties$', + '(^|\\/)gradle\\/.+\\.toml$', + '\\.versions\\.toml$', + ], timeout: 600, versioning: gradleVersioning.id, }; diff --git a/lib/manager/gradle/shallow/__fixtures__/1/libs.versions.toml b/lib/manager/gradle/shallow/__fixtures__/1/libs.versions.toml new file mode 100644 index 0000000000000000000000000000000000000000..60a452bda6ada9e159b7dba7aedb53511ee9a722 --- /dev/null +++ b/lib/manager/gradle/shallow/__fixtures__/1/libs.versions.toml @@ -0,0 +1,17 @@ +[versions] +detekt = "1.17.0" +kotest = "4.6.0" +publish-on-central = "0.5.0" + +[libraries] +detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } +kotest-assertions-core-jvm = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" } +kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } +mockito = { group = "org.mockito", name = "mockito-core", version = "3.10.0" } + +[bundles] +kotest = [ "kotest-runner-junit5", "kotest-assertions-core-jvm" ] + +[plugins] +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +publish-on-central = { id = "org.danilopianini.publish-on-central", version.ref = "publish-on-central" } diff --git a/lib/manager/gradle/shallow/__fixtures__/2/libs.versions.toml b/lib/manager/gradle/shallow/__fixtures__/2/libs.versions.toml new file mode 100644 index 0000000000000000000000000000000000000000..8308a0c81674fa228b7124ee93f5086ed25eaef6 --- /dev/null +++ b/lib/manager/gradle/shallow/__fixtures__/2/libs.versions.toml @@ -0,0 +1,13 @@ +[versions] +kotlin = "1.5.21" +retrofit = "2.8.2" + +[libraries] +okHttp = "com.squareup.okhttp3:okhttp:4.9.0" +okio = { module = "com.squareup.okio:okio", version = "2.8.0" } +picasso = { group = "com.squareup.picasso", name = "picasso", version = "2.5.1" } +retrofit2-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } + +[plugins] +kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version = "1.5.21" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/lib/manager/gradle/shallow/extract.spec.ts b/lib/manager/gradle/shallow/extract.spec.ts index 1456652cd4e7556c0b086e77a8ea7d46ebf6dd8d..55f36525df3f7ded915fb5e97f2bfb93a27a37a9 100644 --- a/lib/manager/gradle/shallow/extract.spec.ts +++ b/lib/manager/gradle/shallow/extract.spec.ts @@ -1,5 +1,5 @@ import { extractAllPackageFiles } from '..'; -import { fs } from '../../../../test/util'; +import { fs, loadFixture } from '../../../../test/util'; jest.mock('../../../util/fs'); @@ -136,4 +136,182 @@ describe('manager/gradle/shallow/extract', () => { }, ]); }); + + it('works with dependency catalogs', async () => { + const tomlFile = loadFixture('1/libs.versions.toml'); + const fsMock = { + 'gradle/libs.versions.toml': tomlFile, + }; + mockFs(fsMock); + const res = await extractAllPackageFiles({} as never, Object.keys(fsMock)); + expect(res).toMatchObject([ + { + packageFile: 'gradle/libs.versions.toml', + deps: [ + { + depName: 'io.gitlab.arturbosch.detekt:detekt-formatting', + groupName: 'io.gitlab.arturbosch.detekt', + currentValue: '1.17.0', + managerData: { + fileReplacePosition: 21, + packageFile: 'gradle/libs.versions.toml', + }, + }, + { + depName: 'io.kotest:kotest-assertions-core-jvm', + groupName: 'io.kotest', + currentValue: '4.6.0', + managerData: { + fileReplacePosition: 39, + packageFile: 'gradle/libs.versions.toml', + }, + }, + { + depName: 'io.kotest:kotest-runner-junit5', + groupName: 'io.kotest', + currentValue: '4.6.0', + managerData: { + fileReplacePosition: 39, + packageFile: 'gradle/libs.versions.toml', + }, + }, + { + depName: 'org.mockito:mockito-core', + groupName: 'org.mockito', + currentValue: '3.10.0', + managerData: { + fileReplacePosition: 460, + packageFile: 'gradle/libs.versions.toml', + }, + }, + { + depName: 'io.gitlab.arturbosch.detekt', + depType: 'plugin', + currentValue: '1.17.0', + commitMessageTopic: 'plugin detekt', + lookupName: + 'io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin', + managerData: { + fileReplacePosition: 21, + packageFile: 'gradle/libs.versions.toml', + }, + registryUrls: [ + 'https://repo.maven.apache.org/maven2', + 'https://plugins.gradle.org/m2/', + ], + }, + { + depName: 'org.danilopianini.publish-on-central', + depType: 'plugin', + currentValue: '0.5.0', + commitMessageTopic: 'plugin publish-on-central', + lookupName: + 'org.danilopianini.publish-on-central:org.danilopianini.publish-on-central.gradle.plugin', + managerData: { + fileReplacePosition: 68, + packageFile: 'gradle/libs.versions.toml', + }, + registryUrls: [ + 'https://repo.maven.apache.org/maven2', + 'https://plugins.gradle.org/m2/', + ], + }, + ], + }, + ]); + }); + + it("can run Javier's example", async () => { + const tomlFile = loadFixture('2/libs.versions.toml'); + const fsMock = { + 'gradle/libs.versions.toml': tomlFile, + }; + mockFs(fsMock); + const res = await extractAllPackageFiles({} as never, Object.keys(fsMock)); + expect(res).toMatchObject([ + { + packageFile: 'gradle/libs.versions.toml', + deps: [ + { + depName: 'com.squareup.okhttp3:okhttp', + groupName: 'com.squareup.okhttp3', + currentValue: '4.9.0', + managerData: { + fileReplacePosition: 99, + packageFile: 'gradle/libs.versions.toml', + }, + }, + { + depName: 'com.squareup.okio:okio', + groupName: 'com.squareup.okio', + currentValue: '2.8.0', + managerData: { + fileReplacePosition: 161, + packageFile: 'gradle/libs.versions.toml', + }, + }, + { + depName: 'com.squareup.picasso:picasso', + groupName: 'com.squareup.picasso', + currentValue: '2.5.1', + managerData: { + fileReplacePosition: 243, + packageFile: 'gradle/libs.versions.toml', + }, + }, + { + depName: 'com.squareup.retrofit2:retrofit', + groupName: 'com.squareup.retrofit2', + currentValue: '2.8.2', + managerData: { + fileReplacePosition: 41, + packageFile: 'gradle/libs.versions.toml', + }, + }, + { + depName: 'org.jetbrains.kotlin.jvm', + depType: 'plugin', + currentValue: '1.5.21', + commitMessageTopic: 'plugin kotlinJvm', + lookupName: + 'org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin', + managerData: { + fileReplacePosition: 415, + packageFile: 'gradle/libs.versions.toml', + }, + registryUrls: [ + 'https://repo.maven.apache.org/maven2', + 'https://plugins.gradle.org/m2/', + ], + }, + { + depName: 'org.jetbrains.kotlin.plugin.serialization', + depType: 'plugin', + currentValue: '1.5.21', + commitMessageTopic: 'plugin kotlinSerialization', + lookupName: + 'org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin', + managerData: { + fileReplacePosition: 21, + packageFile: 'gradle/libs.versions.toml', + }, + registryUrls: [ + 'https://repo.maven.apache.org/maven2', + 'https://plugins.gradle.org/m2/', + ], + }, + ], + }, + ]); + }); + + it('ignores an empty TOML', async () => { + const tomlFile = ''; + const fsMock = { + 'gradle/libs.versions.toml': tomlFile, + }; + mockFs(fsMock); + const res = await extractAllPackageFiles({} as never, Object.keys(fsMock)); + expect(res).toBeNull(); + }); }); diff --git a/lib/manager/gradle/shallow/extract.ts b/lib/manager/gradle/shallow/extract.ts index 7dabe5e1ee59d1dcee4a1bca5c7a63c1271087de..cd9a3161f800a5234a13dc6d8b59e3f410aeb249 100644 --- a/lib/manager/gradle/shallow/extract.ts +++ b/lib/manager/gradle/shallow/extract.ts @@ -11,12 +11,14 @@ import type { PackageFile, } from '../../types'; import type { GradleManagerData } from '../types'; +import { parseCatalog } from './extract/catalog'; import { parseGradle, parseProps } from './parser'; import type { PackageVariables, VariableRegistry } from './types'; import { getVars, isGradleFile, isPropsFile, + isTOMLFile, reorderFiles, toAbsolutePath, } from './utils'; @@ -64,6 +66,13 @@ export async function extractAllPackageFiles( const { vars, deps } = parseProps(content, packageFile); updateVars(vars); extractedDeps.push(...deps); + } else if (isTOMLFile(packageFile)) { + try { + const updatesFromCatalog = parseCatalog(packageFile, content); + extractedDeps.push(...updatesFromCatalog); + } catch (error) { + logger.warn({ error }, 'TOML parsing error'); + } } else if (isGradleFile(packageFile)) { const vars = getVars(registry, dir); const { diff --git a/lib/manager/gradle/shallow/extract/catalog.ts b/lib/manager/gradle/shallow/extract/catalog.ts new file mode 100644 index 0000000000000000000000000000000000000000..0812932b95731cbe056b8b12f59ae38c97ccf143 --- /dev/null +++ b/lib/manager/gradle/shallow/extract/catalog.ts @@ -0,0 +1,83 @@ +import { parse } from '@iarna/toml'; +import { PackageDependency } from '../../../types'; +import { GradleManagerData } from '../../types'; +import type { GradleCatalog, GradleCatalogPluginDescriptor } from '../types'; + +function findIndexAfter( + content: string, + sliceAfter: string, + find: string +): number { + const slicePoint = content.indexOf(sliceAfter) + sliceAfter.length; + return slicePoint + content.slice(slicePoint).indexOf(find); +} + +export function parseCatalog( + packageFile: string, + content: string +): PackageDependency<GradleManagerData>[] { + const tomlContent = parse(content) as GradleCatalog; + const versions = tomlContent.versions || {}; + const libs = tomlContent.libraries || {}; + const libStartIndex = content.indexOf('libraries'); + const libSubContent = content.slice(libStartIndex); + const versionStartIndex = content.indexOf('versions'); + const versionSubContent = content.slice(versionStartIndex); + const extractedDeps: PackageDependency<GradleManagerData>[] = []; + for (const libraryName of Object.keys(libs)) { + const libDescriptor = libs[libraryName]; + const group: string = + typeof libDescriptor === 'string' + ? libDescriptor.split(':')[0] + : libDescriptor.group || libDescriptor.module?.split(':')[0]; + const name: string = + typeof libDescriptor === 'string' + ? libDescriptor.split(':')[1] + : libDescriptor.name || libDescriptor.module?.split(':')[1]; + const version = libDescriptor.version || libDescriptor.split(':')[2]; + const currentVersion = + typeof version === 'string' ? version : versions[version.ref]; + const fileReplacePosition = + typeof version === 'string' + ? libStartIndex + + findIndexAfter(libSubContent, libraryName, currentVersion) + : versionStartIndex + + findIndexAfter(versionSubContent, version.ref, currentVersion); + const dependency = { + depName: `${group}:${name}`, + groupName: group, + currentValue: currentVersion, + managerData: { fileReplacePosition, packageFile }, + }; + extractedDeps.push(dependency); + } + const plugins = tomlContent.plugins || {}; + const pluginsStartIndex = content.indexOf('[plugins]'); + const pluginsSubContent = content.slice(pluginsStartIndex); + for (const pluginName of Object.keys(plugins)) { + const pluginDescriptor = plugins[ + pluginName + ] as GradleCatalogPluginDescriptor; + const pluginId = pluginDescriptor.id; + const version = pluginDescriptor.version; + const currentVersion: string = + typeof version === 'string' ? version : versions[version.ref]; + const fileReplacePosition = + typeof version === 'string' + ? pluginsStartIndex + + findIndexAfter(pluginsSubContent, pluginId, currentVersion) + : versionStartIndex + + findIndexAfter(versionSubContent, version.ref, currentVersion); + const dependency = { + depType: 'plugin', + depName: pluginId, + lookupName: `${pluginId}:${pluginId}.gradle.plugin`, + registryUrls: ['https://plugins.gradle.org/m2/'], + currentValue: currentVersion, + commitMessageTopic: `plugin ${pluginName}`, + managerData: { fileReplacePosition, packageFile }, + }; + extractedDeps.push(dependency); + } + return extractedDeps; +} diff --git a/lib/manager/gradle/shallow/types.ts b/lib/manager/gradle/shallow/types.ts index 4d71036dbbba370ff81b59291917c13ddd9a9c2b..a1867f968ceaf0e4059fc898cb034ff658e54498 100644 --- a/lib/manager/gradle/shallow/types.ts +++ b/lib/manager/gradle/shallow/types.ts @@ -61,3 +61,32 @@ export interface ParseGradleResult { urls: string[]; vars: PackageVariables; } + +export interface GradleCatalog { + versions?: Map<string, string>; + libraries?: Map< + string, + GradleCatalogModuleDescriptor | GradleCatalogArtifactDescriptor | string + >; + plugins?: Map<string, GradleCatalogPluginDescriptor>; +} + +export interface GradleCatalogModuleDescriptor { + module: string; + version: string | VersionPointer; +} + +export interface GradleCatalogArtifactDescriptor { + name: string; + group: string; + version: string | VersionPointer; +} + +export interface GradleCatalogPluginDescriptor { + id: string; + version: string | VersionPointer; +} + +export interface VersionPointer { + ref: string; +} diff --git a/lib/manager/gradle/shallow/utils.ts b/lib/manager/gradle/shallow/utils.ts index 2d8080ef1a43678b304a671c1ab7784b4a1263c7..75bf22405d4c56908222db936b9090f3dd5f935c 100644 --- a/lib/manager/gradle/shallow/utils.ts +++ b/lib/manager/gradle/shallow/utils.ts @@ -81,6 +81,11 @@ export function isPropsFile(path: string): boolean { return filename === 'gradle.properties'; } +export function isTOMLFile(path: string): boolean { + const filename = upath.basename(path).toLowerCase(); + return filename.endsWith('.toml'); +} + export function toAbsolutePath(packageFile: string): string { return upath.join(packageFile.replace(/^[/\\]*/, '/')); }