feat(bazel-module): Add support of maven methods (#30884)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
This commit is contained in:
Aleksei Babich 2024-08-22 15:15:51 +02:00 committed by GitHub
parent d6dd092954
commit 49b7e1fc82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 443 additions and 24 deletions

View file

@ -5,6 +5,7 @@ import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
import { BazelDatasource } from '../../datasource/bazel';
import { GithubTagsDatasource } from '../../datasource/github-tags';
import { MavenDatasource } from '../../datasource/maven';
import * as parser from './parser';
import { extractPackageFile } from '.';
@ -235,5 +236,106 @@ describe('modules/manager/bazel-module/extract', () => {
},
]);
});
it('returns maven.install and maven.artifact dependencies', async () => {
const input = codeBlock`
maven.artifact(
artifact = "core.specs.alpha",
exclusions = ["org.clojure:clojure"],
group = "org.clojure",
version = "0.2.56",
)
maven.install(
artifacts = [
"junit:junit:4.13.2",
"com.google.guava:guava:31.1-jre",
],
lock_file = "//:maven_install.json",
repositories = [
"https://repo1.maven.org/maven2/",
],
version_conflict_policy = "pinned",
)
`;
const result = await extractPackageFile(input, 'MODULE.bazel');
if (!result) {
throw new Error('Expected a result.');
}
expect(result.deps).toEqual([
{
datasource: MavenDatasource.id,
depType: 'maven_install',
depName: 'junit:junit',
currentValue: '4.13.2',
registryUrls: ['https://repo1.maven.org/maven2/'],
versioning: 'gradle',
},
{
datasource: MavenDatasource.id,
depType: 'maven_install',
depName: 'com.google.guava:guava',
currentValue: '31.1-jre',
registryUrls: ['https://repo1.maven.org/maven2/'],
versioning: 'gradle',
},
{
datasource: MavenDatasource.id,
depType: 'maven_install',
depName: 'org.clojure:core.specs.alpha',
currentValue: '0.2.56',
registryUrls: ['https://repo1.maven.org/maven2/'],
versioning: 'gradle',
},
]);
});
it('returns maven.install and bazel_dep dependencies together', async () => {
const input = codeBlock`
bazel_dep(name = "bazel_jar_jar", version = "0.1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
artifacts = [
"junit:junit:4.13.2",
"com.google.guava:guava:31.1-jre",
],
lock_file = "//:maven_install.json",
repositories = [
"https://repo1.maven.org/maven2/",
],
version_conflict_policy = "pinned",
)
`;
const result = await extractPackageFile(input, 'MODULE.bazel');
if (!result) {
throw new Error('Expected a result.');
}
expect(result.deps).toEqual([
{
datasource: BazelDatasource.id,
depType: 'bazel_dep',
depName: 'bazel_jar_jar',
currentValue: '0.1.0',
},
{
datasource: MavenDatasource.id,
depType: 'maven_install',
depName: 'junit:junit',
currentValue: '4.13.2',
registryUrls: ['https://repo1.maven.org/maven2/'],
versioning: 'gradle',
},
{
datasource: MavenDatasource.id,
depType: 'maven_install',
depName: 'com.google.guava:guava',
currentValue: '31.1-jre',
registryUrls: ['https://repo1.maven.org/maven2/'],
versioning: 'gradle',
},
]);
});
});
});

View file

@ -2,9 +2,11 @@ import { dirname } from 'upath';
import { logger } from '../../../logger';
import { isNotNullOrUndefined } from '../../../util/array';
import { LooseArray } from '../../../util/schema-utils';
import type { PackageFileContent } from '../types';
import type { PackageDependency, PackageFileContent } from '../types';
import * as bazelrc from './bazelrc';
import type { RecordFragment } from './fragments';
import { parse } from './parser';
import { RuleToMavenPackageDep, fillRegistryUrls } from './parser/maven';
import { RuleToBazelModulePackageDep } from './rules';
import * as rules from './rules';
@ -14,15 +16,28 @@ export async function extractPackageFile(
): Promise<PackageFileContent | null> {
try {
const records = parse(content);
const pfc: PackageFileContent | null = LooseArray(
RuleToBazelModulePackageDep,
)
.transform(rules.toPackageDependencies)
.transform((deps) => (deps.length ? { deps } : null))
.parse(records);
if (!pfc) {
const pfc = await extractBazelPfc(records, packageFile);
const mavenDeps = extractMavenDeps(records);
if (mavenDeps.length) {
pfc.deps.push(...mavenDeps);
}
return pfc.deps.length ? pfc : null;
} catch (err) {
logger.debug({ err, packageFile }, 'Failed to parse bazel module file.');
return null;
}
}
async function extractBazelPfc(
records: RecordFragment[],
packageFile: string,
): Promise<PackageFileContent> {
const pfc: PackageFileContent = LooseArray(RuleToBazelModulePackageDep)
.transform(rules.toPackageDependencies)
.transform((deps) => ({ deps }))
.parse(records);
const registryUrls = (await bazelrc.read(dirname(packageFile)))
// Ignore any entries for custom configurations
@ -34,8 +49,10 @@ export async function extractPackageFile(
}
return pfc;
} catch (err) {
logger.debug({ err, packageFile }, 'Failed to parse bazel module file.');
return null;
}
}
function extractMavenDeps(records: RecordFragment[]): PackageDependency[] {
return LooseArray(RuleToMavenPackageDep)
.transform(fillRegistryUrls)
.parse(records);
}

View file

@ -21,6 +21,11 @@ export const ArrayFragmentSchema = z.object({
items: LooseArray(PrimitiveFragmentsSchema),
isComplete: z.boolean(),
});
export const StringArrayFragmentSchema = z.object({
type: z.literal('array'),
items: LooseArray(StringFragmentSchema),
isComplete: z.boolean(),
});
const ValueFragmentsSchema = z.discriminatedUnion('type', [
StringFragmentSchema,
BooleanFragmentSchema,

View file

@ -1,6 +1,7 @@
import type { Category } from '../../../constants';
import { BazelDatasource } from '../../datasource/bazel';
import { GithubTagsDatasource } from '../../datasource/github-tags';
import { MavenDatasource } from '../../datasource/maven';
import { extractPackageFile } from './extract';
export { extractPackageFile };
@ -14,4 +15,5 @@ export const categories: Category[] = ['bazel'];
export const supportedDatasources = [
BazelDatasource.id,
GithubTagsDatasource.id,
MavenDatasource.id,
];

View file

@ -66,12 +66,12 @@ describe('modules/manager/bazel-module/parser/index', () => {
{
rule: fragments.string('git_override'),
module_name: fragments.string('rules_foo'),
remote: fragments.string(
'https://github.com/example/rules_foo.git',
),
commit: fragments.string(
'6a2c2e22849b3e6b33d5ea9aa72222d4803a986a',
),
remote: fragments.string(
'https://github.com/example/rules_foo.git',
),
},
true,
),
@ -167,5 +167,116 @@ describe('modules/manager/bazel-module/parser/index', () => {
),
]);
});
it('finds maven.artifact', () => {
const input = codeBlock`
maven.artifact(
artifact = "core.specs.alpha",
exclusions = ["org.clojure:clojure"],
group = "org.clojure",
version = "0.2.56",
)
maven_1.artifact(
artifact = "core.specs.alpha1",
group = "org.clojure1",
version = "0.2.561",
)
`;
const res = parse(input);
expect(res).toEqual([
fragments.record(
{
rule: fragments.string('maven_artifact'),
group: fragments.string('org.clojure'),
artifact: fragments.string('core.specs.alpha'),
version: fragments.string('0.2.56'),
exclusions: fragments.array(
[
{
type: 'string',
value: 'org.clojure:clojure',
isComplete: true,
},
],
true,
),
},
true,
),
fragments.record(
{
rule: fragments.string('maven_artifact'),
group: fragments.string('org.clojure1'),
artifact: fragments.string('core.specs.alpha1'),
version: fragments.string('0.2.561'),
},
true,
),
]);
});
it('finds maven.install and maven.artifact', () => {
const input = codeBlock`
maven.install(
artifacts = [
"junit:junit:4.13.2",
"com.google.guava:guava:31.1-jre",
],
repositories = [
"https://repo1.maven.org/maven2/"
]
)
maven.artifact(
artifact = "core.specs.alpha",
group = "org.clojure",
version = "0.2.56",
)
`;
const res = parse(input);
expect(res).toEqual([
fragments.record(
{
rule: fragments.string('maven_install'),
artifacts: fragments.array(
[
{
type: 'string',
value: 'junit:junit:4.13.2',
isComplete: true,
},
{
type: 'string',
value: 'com.google.guava:guava:31.1-jre',
isComplete: true,
},
],
true,
),
repositories: fragments.array(
[
{
type: 'string',
value: 'https://repo1.maven.org/maven2/',
isComplete: true,
},
],
true,
),
},
true,
),
fragments.record(
{
rule: fragments.string('maven_artifact'),
group: fragments.string('org.clojure'),
artifact: fragments.string('core.specs.alpha'),
version: fragments.string('0.2.56'),
},
true,
),
]);
});
});
});

View file

@ -1,9 +1,10 @@
import { lang, query as q } from 'good-enough-parser';
import { Ctx } from '../context';
import type { RecordFragment } from '../fragments';
import { mavenRules } from './maven';
import { moduleRules } from './module';
const rule = q.alt<Ctx>(moduleRules);
const rule = q.alt<Ctx>(moduleRules, mavenRules);
const query = q.tree<Ctx>({
type: 'root-tree',

View file

@ -0,0 +1,153 @@
import { query as q } from 'good-enough-parser';
import { z } from 'zod';
import { regEx } from '../../../../util/regex';
import { MavenDatasource } from '../../../datasource/maven';
import { id as versioning } from '../../../versioning/gradle';
import type { PackageDependency } from '../../types';
import type { Ctx } from '../context';
import {
RecordFragmentSchema,
StringArrayFragmentSchema,
StringFragmentSchema,
} from '../fragments';
const artifactMethod = 'artifact';
const installMethod = 'install';
const commonDepType = 'maven_install';
const mavenVariableRegex = regEx(/^maven.*/);
const bzlmodMavenMethods = [installMethod, artifactMethod];
const methodRegex = regEx(`^${bzlmodMavenMethods.join('|')}$`);
function getParsedRuleByMethod(method: string): string {
return `maven_${method}`;
}
const ArtifactSpec = z.object({
group: z.string(),
artifact: z.string(),
version: z.string(),
});
type ArtifactSpec = z.infer<typeof ArtifactSpec>;
const MavenArtifactTarget = RecordFragmentSchema.extend({
children: z.object({
rule: StringFragmentSchema.extend({
value: z.literal(getParsedRuleByMethod(artifactMethod)),
}),
artifact: StringFragmentSchema,
group: StringFragmentSchema,
version: StringFragmentSchema,
}),
}).transform(
({ children: { rule, artifact, group, version } }): PackageDependency[] => [
{
datasource: MavenDatasource.id,
versioning,
depName: `${group.value}:${artifact.value}`,
currentValue: version.value,
depType: rule.value,
},
],
);
const MavenInstallTarget = RecordFragmentSchema.extend({
children: z.object({
rule: StringFragmentSchema.extend({
value: z.literal(getParsedRuleByMethod(installMethod)),
}),
artifacts: StringArrayFragmentSchema.transform((artifacts) => {
const result: ArtifactSpec[] = [];
for (const { value } of artifacts.items) {
const [group, artifact, version] = value.split(':');
if (group && artifact && version) {
result.push({ group, artifact, version });
}
}
return result;
}),
repositories: StringArrayFragmentSchema,
}),
}).transform(
({ children: { rule, artifacts, repositories } }): PackageDependency[] =>
artifacts.map(({ group, artifact, version: currentValue }) => ({
datasource: MavenDatasource.id,
versioning,
depName: `${group}:${artifact}`,
currentValue,
depType: rule.value,
registryUrls: repositories.items.map((i) => i.value),
})),
);
export const RuleToMavenPackageDep = z.union([
MavenArtifactTarget,
MavenInstallTarget,
]);
export function fillRegistryUrls(
packageDeps: PackageDependency[][],
): PackageDependency[] {
const artifactRules: PackageDependency[] = [];
const registryUrls: string[] = [];
const result: PackageDependency[] = [];
// registry urls are specified only in maven.install, not in maven.artifact
packageDeps.flat().forEach((dep) => {
if (dep.depType === getParsedRuleByMethod(installMethod)) {
if (Array.isArray(dep.registryUrls)) {
registryUrls.push(...dep.registryUrls);
result.push(dep);
}
} else if (dep.depType === getParsedRuleByMethod(artifactMethod)) {
artifactRules.push(dep);
}
});
const uniqUrls = [...new Set(registryUrls)];
for (const artifactRule of artifactRules) {
artifactRule.registryUrls = uniqUrls;
artifactRule.depType = commonDepType;
result.push(artifactRule);
}
return result;
}
const kvParams = q
.sym<Ctx>((ctx, token) => ctx.startAttribute(token.value))
.op('=')
.alt(
q.str((ctx, token) => ctx.addString(token.value)),
q.tree({
type: 'wrapped-tree',
maxDepth: 1,
startsWith: '[',
endsWith: ']',
postHandler: (ctx) => ctx.endArray(),
preHandler: (ctx) => ctx.startArray(),
search: q.many(q.str<Ctx>((ctx, token) => ctx.addString(token.value))),
}),
);
export const mavenRules = q
.sym<Ctx>(mavenVariableRegex, (ctx, token) => {
return ctx.startRule(token.value);
})
.op('.')
.sym(methodRegex, (ctx, token) => {
const rule = ctx.currentRecord.children.rule;
if (rule.type === 'string') {
rule.value = getParsedRuleByMethod(token.value);
}
return ctx;
})
.join(
q.tree({
type: 'wrapped-tree',
maxDepth: 1,
search: kvParams,
postHandler: (ctx) => ctx.endRule(),
}),
);

View file

@ -16,6 +16,7 @@ const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`);
/**
* Matches key-value pairs:
* - `name = "foobar"`
* - `name = True`
**/
const kvParams = q
.sym<Ctx>((ctx, token) => ctx.startAttribute(token.value))
@ -32,6 +33,6 @@ export const moduleRules = q
type: 'wrapped-tree',
maxDepth: 1,
search: kvParams,
postHandler: (ctx, tree) => ctx.endRule(),
postHandler: (ctx) => ctx.endRule(),
}),
);

View file

@ -1 +1,28 @@
The `bazel-module` manager can update [Bazel module (bzlmod)](https://bazel.build/external/module) enabled workspaces.
It also takes care about maven artifacts initalized with [bzlmod](https://github.com/bazelbuild/rules_jvm_external/blob/master/docs/bzlmod.md). For simplicity the name of extension variable is limited to `maven*`. E.g.:
```
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
```
```
maven_1 = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
```
Both `install` and `artifact` methods are supported:
```
maven.install(
artifacts = [
"org.seleniumhq.selenium:selenium-java:4.4.0",
],
)
maven.artifact(
artifact = "javapoet",
group = "com.squareup",
neverlink = True,
version = "1.11.1",
)
```