Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gradle): add support for gradle repository content descriptors #33692

Merged
merged 7 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
212 changes: 211 additions & 1 deletion lib/modules/manager/gradle/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { codeBlock } from 'common-tags';
import { Fixtures } from '../../../../test/fixtures';
import { fs, logger, partial } from '../../../../test/util';
import type { ExtractConfig } from '../types';
import type { ExtractConfig, PackageDependency } from '../types';
import { matchesContentDescriptor } from './extract';
import * as parser from './parser';
import { extractAllPackageFiles } from '.';

Expand Down Expand Up @@ -494,6 +495,215 @@ describe('modules/manager/gradle/extract', () => {
},
]);
});

describe('content descriptors', () => {
describe('simple descriptor matches', () => {
it.each`
input | output | descriptor
${'foo:bar:1.2.3'} | ${true} | ${undefined}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'bar' }]}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo', artifactId: 'bar' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'baz' }]}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.3' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.3' }]}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.+' }]}
${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'baz', version: '4.5.6' }]}
${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo' }]}
${'foo.bar.baz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.bar.baz' }]}
${'foo.bar.baz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.bar' }]}
${'foo.bar.baz:qux:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.barbaz' }]}
${'foobarbaz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*' }]}
${'foobarbaz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*', artifactId: 'qux' }]}
${'foobar:foobar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*', artifactId: 'foo.*' }]}
${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^bar' }]}
${'foobar:foobar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^foo.*', version: '1\\.*' }]}
${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^foo', version: '3.+' }]}
${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: 'qux', version: '1\\.*' }]}
`('$input | $output', ({ input, output, descriptor }) => {
const [groupId, artifactId, currentValue] = input.split(':');
const dep: PackageDependency = {
depName: `${groupId}:${artifactId}`,
currentValue,
};

expect(matchesContentDescriptor(dep, descriptor)).toBe(output);
});
});

describe('multiple descriptors', () => {
const dep: PackageDependency = {
depName: `foo:bar`,
currentValue: '1.2.3',
};

it('if both includes and excludes exist, dep must match include and not match exclude', () => {
expect(
matchesContentDescriptor(dep, [
{ mode: 'include', matcher: 'simple', groupId: 'foo' },
{
mode: 'exclude',
matcher: 'simple',
groupId: 'foo',
artifactId: 'baz',
},
]),
).toBe(true);

expect(
matchesContentDescriptor(dep, [
{ mode: 'include', matcher: 'simple', groupId: 'foo' },
{
mode: 'exclude',
matcher: 'simple',
groupId: 'foo',
artifactId: 'bar',
},
]),
).toBe(false);
});

it('if only includes exist, dep must match at least one include', () => {
expect(
matchesContentDescriptor(dep, [
{ mode: 'include', matcher: 'simple', groupId: 'some' },
{ mode: 'include', matcher: 'simple', groupId: 'foo' },
{ mode: 'include', matcher: 'simple', groupId: 'bar' },
]),
).toBe(true);

expect(
matchesContentDescriptor(dep, [
{ mode: 'include', matcher: 'simple', groupId: 'some' },
{ mode: 'include', matcher: 'simple', groupId: 'other' },
{ mode: 'include', matcher: 'simple', groupId: 'bar' },
]),
).toBe(false);
});

it('if only excludes exist, dep must match not match any exclude', () => {
expect(
matchesContentDescriptor(dep, [
{ mode: 'exclude', matcher: 'simple', groupId: 'some' },
{ mode: 'exclude', matcher: 'simple', groupId: 'foo' },
{ mode: 'exclude', matcher: 'simple', groupId: 'bar' },
]),
).toBe(false);

expect(
matchesContentDescriptor(dep, [
{ mode: 'exclude', matcher: 'simple', groupId: 'some' },
{ mode: 'exclude', matcher: 'simple', groupId: 'other' },
{ mode: 'exclude', matcher: 'simple', groupId: 'bar' },
]),
).toBe(true);
});
});

it('extracts content descriptors', async () => {
const fsMock = {
'build.gradle': codeBlock`
pluginManagement {
repositories {
maven {
url = "https://foo.bar/baz"
content {
includeModule("com.diffplug.spotless", "com.diffplug.spotless.gradle.plugin")
}
}
}
}
repositories {
mavenCentral()
google {
content {
includeGroupAndSubgroups("foo.bar")
includeModuleByRegex("com\\\\.(google|android).*", "protobuf.*")
includeGroupByRegex("(?!(unsupported|pattern).*)")
includeGroupByRegex "org\\\\.jetbrains\\\\.kotlin.*"
excludeModule("foo.bar.group", "simple.module")
}
}
maven {
name = "some"
url = "https://foo.bar/\${name}"
content {
includeModule("foo.bar.group", "simple.module")
includeVersion("com.google.protobuf", "protobuf-java", "2.17.+")
}
}
}

plugins {
id("com.diffplug.spotless") version "6.10.0"
}

dependencies {
implementation "com.google.protobuf:protobuf-java:2.17.1"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21"
implementation "foo.bar:protobuf-java:2.17.0"
implementation "foo.bar.group:simple.module:2.17.0"
}
`,
};
mockFs(fsMock);

const res = await extractAllPackageFiles(
partial<ExtractConfig>(),
Object.keys(fsMock),
);

expect(res).toMatchObject([
{
deps: [
{
depName: 'com.diffplug.spotless',
currentValue: '6.10.0',
depType: 'plugin',
packageName:
'com.diffplug.spotless:com.diffplug.spotless.gradle.plugin',
registryUrls: ['https://foo.bar/baz'],
},
{
depName: 'com.google.protobuf:protobuf-java',
currentValue: '2.17.1',
registryUrls: [
'https://repo.maven.apache.org/maven2',
'https://dl.google.com/android/maven2/',
'https://foo.bar/some',
],
},
{
depName: 'org.jetbrains.kotlin:kotlin-stdlib-jdk8',
currentValue: '1.4.21',
registryUrls: [
'https://repo.maven.apache.org/maven2',
'https://dl.google.com/android/maven2/',
],
},
{
depName: 'foo.bar:protobuf-java',
currentValue: '2.17.0',
registryUrls: [
'https://repo.maven.apache.org/maven2',
'https://dl.google.com/android/maven2/',
],
},
{
depName: 'foo.bar.group:simple.module',
currentValue: '2.17.0',
registryUrls: [
'https://repo.maven.apache.org/maven2',
'https://foo.bar/some',
],
},
],
},
]);
});
});
});

describe('version catalogs', () => {
Expand Down
88 changes: 88 additions & 0 deletions lib/modules/manager/gradle/extract.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import upath from 'upath';
import { logger } from '../../../logger';
import { coerceArray } from '../../../util/array';
import { getLocalFiles } from '../../../util/fs';
import { regEx } from '../../../util/regex';
import { MavenDatasource } from '../../datasource/maven';
import gradleVersioning from '../../versioning/gradle';
import type { ExtractConfig, PackageDependency, PackageFile } from '../types';
import { parseCatalog } from './extract/catalog';
import {
Expand All @@ -12,6 +15,7 @@ import {
import { parseGradle, parseKotlinSource, parseProps } from './parser';
import { REGISTRY_URLS } from './parser/common';
import type {
ContentDescriptorSpec,
GradleManagerData,
PackageRegistry,
VariableRegistry,
Expand Down Expand Up @@ -44,6 +48,89 @@ function updatePackageRegistries(
}
}

export function matchesContentDescriptor(
dep: PackageDependency<GradleManagerData>,
contentDescriptors?: ContentDescriptorSpec[],
): boolean {
const [groupId, artifactId] = (dep.packageName ?? dep.depName!).split(':');
let hasIncludes = false;
let hasExcludes = false;
let matchesInclude = false;
let matchesExclude = false;

for (const content of coerceArray(contentDescriptors)) {
const {
mode,
matcher,
groupId: contentGroupId,
artifactId: contentArtifactId,
version: contentVersion,
} = content;

// group matching
let groupMatch = false;
if (matcher === 'regex') {
groupMatch = regEx(contentGroupId).test(groupId);
} else if (matcher === 'subgroup') {
groupMatch =
groupId === contentGroupId || `${groupId}.`.startsWith(contentGroupId);
} else {
groupMatch = groupId === contentGroupId;
}

// artifact matching (optional)
let artifactMatch = true;
if (groupMatch && contentArtifactId) {
if (matcher === 'regex') {
artifactMatch = regEx(contentArtifactId).test(artifactId);
} else {
artifactMatch = artifactId === contentArtifactId;
}
}

// version matching (optional)
let versionMatch = true;
if (groupMatch && artifactMatch && contentVersion && dep.currentValue) {
if (matcher === 'regex') {
versionMatch = regEx(contentVersion).test(dep.currentValue);
} else {
// contentVersion can be an exact version or a gradle-supported version range
versionMatch = gradleVersioning.matches(
dep.currentValue,
contentVersion,
);
}
}

const isMatch = groupMatch && artifactMatch && versionMatch;
if (mode === 'include') {
hasIncludes = true;
if (isMatch) {
matchesInclude = true;
}
} else if (mode === 'exclude') {
hasExcludes = true;
if (isMatch) {
matchesExclude = true;
}
}
}

if (hasIncludes && hasExcludes) {
// if both includes and excludes exist, dep must match include and not match exclude
return matchesInclude && !matchesExclude;
} else if (hasIncludes) {
// if only includes exist, dep must match at least one include
return matchesInclude;
} else if (hasExcludes) {
// if only excludes exist, dep must not match any exclude
return !matchesExclude;
}

// by default, repositories include everything and exclude nothing
return true;
}

function getRegistryUrlsForDep(
packageRegistries: PackageRegistry[],
dep: PackageDependency<GradleManagerData>,
Expand All @@ -52,6 +139,7 @@ function getRegistryUrlsForDep(

const registryUrls = packageRegistries
.filter((item) => item.scope === scope)
.filter((item) => matchesContentDescriptor(dep, item.content))
.map((item) => item.registryUrl);

if (!registryUrls.length && scope === 'plugin') {
Expand Down
Loading