diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index 15776b4035680670d6ff21be95bfc7d9a4c653b0..5e82c5682c35ab5aaa9440c19c0dc8142369254c 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -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', + }, + ]); + }); }); }); diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index a6b298eb26d4d26e91cd6cfdaaeb1396e4f65eed..399d471ca2b3fbc3003345d7709c10a86070dd4a 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -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,28 +16,43 @@ 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) { - return null; - } + const pfc = await extractBazelPfc(records, packageFile); + const mavenDeps = extractMavenDeps(records); - const registryUrls = (await bazelrc.read(dirname(packageFile))) - // Ignore any entries for custom configurations - .filter((ce) => ce.config === undefined) - .map((ce) => ce.getOption('registry')?.value) - .filter(isNotNullOrUndefined); - if (registryUrls.length) { - pfc.registryUrls = registryUrls; + if (mavenDeps.length) { + pfc.deps.push(...mavenDeps); } - return pfc; + 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 + .filter((ce) => ce.config === undefined) + .map((ce) => ce.getOption('registry')?.value) + .filter(isNotNullOrUndefined); + if (registryUrls.length) { + pfc.registryUrls = registryUrls; + } + + return pfc; +} + +function extractMavenDeps(records: RecordFragment[]): PackageDependency[] { + return LooseArray(RuleToMavenPackageDep) + .transform(fillRegistryUrls) + .parse(records); +} diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index a5e74aa1092a29a00a11f20ff7a2142a8d30e1cb..806a9beb15ec60b81f79e45d3060deb60cb9efcb 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -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, diff --git a/lib/modules/manager/bazel-module/index.ts b/lib/modules/manager/bazel-module/index.ts index 021ef601adab08acae79ac8b6f2e352e57bf56e4..d3a8e798591d89d885a27857f543a3be3951a7bc 100644 --- a/lib/modules/manager/bazel-module/index.ts +++ b/lib/modules/manager/bazel-module/index.ts @@ -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, ]; diff --git a/lib/modules/manager/bazel-module/parser/index.spec.ts b/lib/modules/manager/bazel-module/parser/index.spec.ts index 2424fa8018c4e45125a13de1c037f5b5152f328d..f0137c9f13a09cd8d04c14bb6fd41e7b89b7ef32 100644 --- a/lib/modules/manager/bazel-module/parser/index.spec.ts +++ b/lib/modules/manager/bazel-module/parser/index.spec.ts @@ -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, + ), + ]); + }); }); }); diff --git a/lib/modules/manager/bazel-module/parser/index.ts b/lib/modules/manager/bazel-module/parser/index.ts index e98d997e8754de32aa6c5809b7113a32b0f4153c..0757a63687557b9d0d7a5436f647fbfd865335e6 100644 --- a/lib/modules/manager/bazel-module/parser/index.ts +++ b/lib/modules/manager/bazel-module/parser/index.ts @@ -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', diff --git a/lib/modules/manager/bazel-module/parser/maven.ts b/lib/modules/manager/bazel-module/parser/maven.ts new file mode 100644 index 0000000000000000000000000000000000000000..9248297c9a461392ba030492631c5e05a8983a3f --- /dev/null +++ b/lib/modules/manager/bazel-module/parser/maven.ts @@ -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(), + }), + ); diff --git a/lib/modules/manager/bazel-module/parser/module.ts b/lib/modules/manager/bazel-module/parser/module.ts index 0c75234b6286204123d00a712e3a3a606e99f81c..ace1d413d7ab8f28db49523ae942f12fa07ae5f6 100644 --- a/lib/modules/manager/bazel-module/parser/module.ts +++ b/lib/modules/manager/bazel-module/parser/module.ts @@ -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(), }), ); diff --git a/lib/modules/manager/bazel-module/readme.md b/lib/modules/manager/bazel-module/readme.md index 6ba11d6580039851cdf43728c95bdacd24bef360..b5b439ba9f0c3696bfbc4493a20685eeae55ab39 100644 --- a/lib/modules/manager/bazel-module/readme.md +++ b/lib/modules/manager/bazel-module/readme.md @@ -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", +) +```