diff --git a/lib/modules/manager/gradle/extract.spec.ts b/lib/modules/manager/gradle/extract.spec.ts index 249c959102a603c882c1170c9f16206545e8c105..c63b0e3a6634eb10ad7dc596bdea314cdeac75b7 100644 --- a/lib/modules/manager/gradle/extract.spec.ts +++ b/lib/modules/manager/gradle/extract.spec.ts @@ -242,11 +242,7 @@ describe('modules/manager/gradle/extract', () => { deps: [ { depType: 'plugin', - registryUrls: [ - 'https://repo.maven.apache.org/maven2', - 'https://example.com', - 'https://plugins.gradle.org/m2/', - ], + registryUrls: ['https://plugins.gradle.org/m2/'], }, { registryUrls: [ @@ -314,6 +310,65 @@ describe('modules/manager/gradle/extract', () => { }, ]); }); + + it('supports separate registry URLs for plugins', async () => { + const settingsFile = codeBlock` + pluginManagement { + repositories { + mavenLocal() + maven { url = "https://foo.bar/plugins" } + } + } + `; + + const buildFile = codeBlock` + plugins { + id "foo.bar" version "1.2.3" + } + repositories { + maven { url = "https://foo.bar/deps" } + mavenCentral() + } + dependencies { + classpath "io.jsonwebtoken:jjwt-api:0.11.2" + } + `; + + const fsMock = { + 'build.gradle': buildFile, + 'settings.gradle': settingsFile, + }; + mockFs(fsMock); + + const res = await extractAllPackageFiles( + {} as ExtractConfig, + Object.keys(fsMock) + ); + + expect(res).toMatchObject([ + { + packageFile: 'settings.gradle', + deps: [], + }, + { + packageFile: 'build.gradle', + deps: [ + { + depName: 'foo.bar', + depType: 'plugin', + registryUrls: ['https://foo.bar/plugins'], + }, + { + depName: 'io.jsonwebtoken:jjwt-api', + registryUrls: [ + 'https://foo.bar/deps', + 'https://repo.maven.apache.org/maven2', + ], + }, + ], + }, + ]); + }); }); describe('version catalogs', () => { diff --git a/lib/modules/manager/gradle/extract.ts b/lib/modules/manager/gradle/extract.ts index f3ff86968dd9511370e4599bd798d63a4409708a..7053716f06ed30af009a00442c8ea73f51fd3d23 100644 --- a/lib/modules/manager/gradle/extract.ts +++ b/lib/modules/manager/gradle/extract.ts @@ -10,8 +10,10 @@ import { usesGcv, } from './extract/consistent-versions-plugin'; import { parseGradle, parseProps } from './parser'; +import { REGISTRY_URLS } from './parser/common'; import type { GradleManagerData, + PackageRegistry, PackageVariables, VariableRegistry, } from './types'; @@ -26,6 +28,23 @@ import { const datasource = MavenDatasource.id; +function getRegistryUrlsForDep( + packageRegistries: PackageRegistry[], + dep: PackageDependency<GradleManagerData> +): string[] { + const scope = dep.depType === 'plugin' ? 'plugin' : 'dep'; + + const registryUrls = packageRegistries + .filter((item) => item.scope === scope) + .map((item) => item.registryUrl); + + if (!registryUrls.length && scope === 'plugin') { + registryUrls.push(REGISTRY_URLS.gradlePluginPortal); + } + + return [...new Set(registryUrls)]; +} + export async function extractAllPackageFiles( config: ExtractConfig, packageFiles: string[] @@ -33,7 +52,7 @@ export async function extractAllPackageFiles( const extractedDeps: PackageDependency<GradleManagerData>[] = []; const varRegistry: VariableRegistry = {}; const packageFilesByName: Record<string, PackageFile> = {}; - const packageRegistries: string[] = []; + const packageRegistries: PackageRegistry[] = []; const reorderedFiles = reorderFiles(packageFiles); const fileContents = await getFileContentMap(packageFiles, true); @@ -75,7 +94,11 @@ export async function extractAllPackageFiles( vars: gradleVars, } = parseGradle(content, vars, packageFile, fileContents); for (const url of urls) { - if (!packageRegistries.includes(url)) { + const registryAlreadyKnown = packageRegistries.some( + (item) => + item.registryUrl === url.registryUrl && item.scope === url.scope + ); + if (!registryAlreadyKnown) { packageRegistries.push(url); } } @@ -114,9 +137,7 @@ export async function extractAllPackageFiles( }; } - dep.registryUrls = [ - ...new Set([...packageRegistries, ...(dep.registryUrls ?? [])]), - ]; + dep.registryUrls = getRegistryUrlsForDep(packageRegistries, dep); if (!dep.depType) { dep.depType = key.startsWith('buildSrc') diff --git a/lib/modules/manager/gradle/extract/catalog.ts b/lib/modules/manager/gradle/extract/catalog.ts index 69d7bb10c9d5ebc520b1d580418bc52ff861dc98..49b21ccceea7bc1f545a21f4eda01013089492b6 100644 --- a/lib/modules/manager/gradle/extract/catalog.ts +++ b/lib/modules/manager/gradle/extract/catalog.ts @@ -276,7 +276,6 @@ export function parseCatalog( depType: 'plugin', depName, packageName: `${depName}:${depName}.gradle.plugin`, - registryUrls: ['https://plugins.gradle.org/m2/'], currentValue, commitMessageTopic: `plugin ${pluginName}`, managerData: { fileReplacePosition }, diff --git a/lib/modules/manager/gradle/parser.spec.ts b/lib/modules/manager/gradle/parser.spec.ts index 4734a6cbca7e0c59495f50cb8979f550deb9400d..efeecbbd0c1f59157609b7da52e1fdebf5eae902 100644 --- a/lib/modules/manager/gradle/parser.spec.ts +++ b/lib/modules/manager/gradle/parser.spec.ts @@ -545,7 +545,7 @@ describe('modules/manager/gradle/parser', () => { ${'jcenter()'} | ${REGISTRY_URLS.jcenter} `('$input', ({ input, output }) => { const { urls } = parseGradle(input); - expect(urls).toStrictEqual([output].filter(Boolean)); + expect(urls).toMatchObject([{ registryUrl: output }]); }); }); @@ -587,11 +587,60 @@ describe('modules/manager/gradle/parser', () => { ${''} | ${'maven { setUrl("foo", "bar") }'} | ${null} ${'base="https://foo.bar"'} | ${'publishing { repositories { maven("${base}/baz") } }'} | ${null} `('$def | $input', ({ def, input, url }) => { - const expected = [url].filter(Boolean); + const expected = url ? [{ registryUrl: url }] : []; const { urls } = parseGradle([def, input].join('\n')); - expect(urls).toStrictEqual(expected); + expect(urls).toMatchObject(expected); }); }); + + it('pluginManagement', () => { + const input = codeBlock` + pluginManagement { + def fooVersion = "1.2.3" + repositories { + mavenLocal() + maven { url = "https://foo.bar/plugins" } + gradlePluginPortal() + } + plugins { + id("foo.bar") version "$fooVersion" + } + } + dependencyResolutionManagement { + repositories { + maven { url = "https://foo.bar/deps" } + mavenCentral() + } + } + `; + + const { deps, urls } = parseGradle(input); + expect(deps).toMatchObject([ + { + depType: 'plugin', + depName: 'foo.bar', + currentValue: '1.2.3', + }, + ]); + expect(urls).toMatchObject([ + { + registryUrl: 'https://foo.bar/plugins', + scope: 'plugin', + }, + { + registryUrl: REGISTRY_URLS.gradlePluginPortal, + scope: 'plugin', + }, + { + registryUrl: 'https://foo.bar/deps', + scope: 'dep', + }, + { + registryUrl: REGISTRY_URLS.mavenCentral, + scope: 'dep', + }, + ]); + }); }); describe('version catalog', () => { diff --git a/lib/modules/manager/gradle/parser.ts b/lib/modules/manager/gradle/parser.ts index ec245fa0c07a68feb19ad6e5e44cb5b2459a2d96..44fd6066b4f2c831ebbf878e3b371d0219eb88f7 100644 --- a/lib/modules/manager/gradle/parser.ts +++ b/lib/modules/manager/gradle/parser.ts @@ -10,6 +10,7 @@ import { qVersionCatalogs } from './parser/version-catalogs'; import type { Ctx, GradleManagerData, + PackageRegistry, PackageVariables, ParseGradleResult, } from './types'; @@ -26,7 +27,7 @@ export function parseGradle( ): ParseGradleResult { let vars: PackageVariables = { ...initVars }; const deps: PackageDependency<GradleManagerData>[] = []; - const urls: string[] = []; + const urls: PackageRegistry[] = []; const query = q.tree<Ctx>({ type: 'root-tree', diff --git a/lib/modules/manager/gradle/parser/handlers.ts b/lib/modules/manager/gradle/parser/handlers.ts index 043a896ddcaf5b23dfead1e07b67cf93e834a10a..c4721ead81db1bb4f521ef5cc7b6030d03a41cf2 100644 --- a/lib/modules/manager/gradle/parser/handlers.ts +++ b/lib/modules/manager/gradle/parser/handlers.ts @@ -216,7 +216,6 @@ export function handlePlugin(ctx: Ctx): Ctx { depType: 'plugin', depName, packageName, - registryUrls: [REGISTRY_URLS.gradlePluginPortal], commitMessageTopic: `plugin ${depName}`, currentValue: pluginVersion[0].value, managerData: { @@ -246,11 +245,22 @@ export function handlePlugin(ctx: Ctx): Ctx { return ctx; } +function isPluginRegistry(ctx: Ctx): boolean { + if (ctx.tokenMap.registryScope) { + const registryScope = loadFromTokenMap(ctx, 'registryScope')[0].value; + return registryScope === 'pluginManagement'; + } + + return false; +} + export function handlePredefinedRegistryUrl(ctx: Ctx): Ctx { const registryName = loadFromTokenMap(ctx, 'registryUrl')[0].value; - ctx.registryUrls.push( - REGISTRY_URLS[registryName as keyof typeof REGISTRY_URLS] - ); + + ctx.registryUrls.push({ + registryUrl: REGISTRY_URLS[registryName as keyof typeof REGISTRY_URLS], + scope: isPluginRegistry(ctx) ? 'plugin' : 'dep', + }); return ctx; } @@ -281,7 +291,10 @@ export function handleCustomRegistryUrl(ctx: Ctx): Ctx { try { const { host, protocol } = url.parse(registryUrl); if (host && protocol) { - ctx.registryUrls.push(registryUrl); + ctx.registryUrls.push({ + registryUrl, + scope: isPluginRegistry(ctx) ? 'plugin' : 'dep', + }); } } catch (e) { // no-op diff --git a/lib/modules/manager/gradle/parser/registry-urls.ts b/lib/modules/manager/gradle/parser/registry-urls.ts index 7a2011b7e46203b0993e79e20bfca25797101bec..3cae44e44b888447ac009cad8bd98cf8428b5674 100644 --- a/lib/modules/manager/gradle/parser/registry-urls.ts +++ b/lib/modules/manager/gradle/parser/registry-urls.ts @@ -1,6 +1,8 @@ import { query as q } from 'good-enough-parser'; import { regEx } from '../../../../util/regex'; import type { Ctx } from '../types'; +import { qApplyFrom } from './apply-from'; +import { qAssignments } from './assignments'; import { REGISTRY_URLS, cleanupTempVars, @@ -13,6 +15,7 @@ import { handleCustomRegistryUrl, handlePredefinedRegistryUrl, } from './handlers'; +import { qPlugins } from './plugins'; // mavenCentral() // mavenCentral { ... } @@ -94,8 +97,38 @@ const qCustomRegistryUrl = q .handler(handleCustomRegistryUrl) .handler(cleanupTempVars); +const qPluginManagement = q.sym<Ctx>('pluginManagement', storeVarToken).tree({ + type: 'wrapped-tree', + startsWith: '{', + endsWith: '}', + preHandler: (ctx) => { + ctx.tmpTokenStore.registryScope = ctx.varTokens; + ctx.varTokens = []; + return ctx; + }, + search: q + .handler<Ctx>((ctx) => { + if (ctx.tmpTokenStore.registryScope) { + ctx.tokenMap.registryScope = ctx.tmpTokenStore.registryScope; + } + return ctx; + }) + .alt( + qAssignments, + qApplyFrom, + qPlugins, + qPredefinedRegistries, + qCustomRegistryUrl + ), + postHandler: (ctx) => { + delete ctx.tmpTokenStore.registryScope; + return ctx; + }, +}); + export const qRegistryUrls = q.alt<Ctx>( q.sym<Ctx>('publishing').tree(), + qPluginManagement, qPredefinedRegistries, qCustomRegistryUrl ); diff --git a/lib/modules/manager/gradle/types.ts b/lib/modules/manager/gradle/types.ts index d68c39cd30dba6fe938997610bc4e14e76bbb426..a4646dbef8ab8619c1c57b93fee1ee330493ceff 100644 --- a/lib/modules/manager/gradle/types.ts +++ b/lib/modules/manager/gradle/types.ts @@ -16,7 +16,7 @@ export type VariableRegistry = Record<string, PackageVariables>; export interface ParseGradleResult { deps: PackageDependency<GradleManagerData>[]; - urls: string[]; + urls: PackageRegistry[]; vars: PackageVariables; } @@ -67,6 +67,11 @@ export interface RichVersion { export type GradleVersionPointerTarget = string | RichVersion; export type GradleVersionCatalogVersion = string | VersionPointer | RichVersion; +export interface PackageRegistry { + registryUrl: string; + scope: 'dep' | 'plugin'; +} + export interface Ctx { readonly packageFile: string; readonly fileContents: Record<string, string | null>; @@ -74,7 +79,7 @@ export interface Ctx { globalVars: PackageVariables; deps: PackageDependency<GradleManagerData>[]; - registryUrls: string[]; + registryUrls: PackageRegistry[]; varTokens: lexer.Token[]; tmpTokenStore: Record<string, lexer.Token[]>;