Skip to content

Commit

Permalink
Support for plugin gradle-consistent-versions by Palantir
Browse files Browse the repository at this point in the history
Fixes renovatebot#8017

Signed-off-by: Jan Høydahl <janhoy@users.noreply.github.com>
  • Loading branch information
janhoy committed Nov 29, 2022
1 parent b35f266 commit 33b0ca9
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/usage/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Renovate can update:
- dependencies whose version is defined in a `*.properties` file
- `*.versions.toml` files in any directory or `*.toml` files inside the `gradle`
directory ([Gradle Version Catalogs docs](https://docs.gradle.org/current/userguide/platforms.html))
- `versions.props` from [gradle-consistent-versions](https://github.com/palantir/gradle-consistent-versions) plugin

Renovate does not support:

Expand Down
17 changes: 14 additions & 3 deletions lib/modules/manager/gradle/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,19 @@ import {
prepareGradleCommand,
} from '../gradle-wrapper/utils';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import {
VERSIONS_LOCK,
isGcvPropsFile,
} from './extract/consistentVersionsPlugin';
import { isGradleBuildFile } from './utils';

// .lockfile is gradle default lockfile, versions.lock is gradle-consistent-versions lockfile
const isLockFile = function (fileName: string): boolean {
return ['.lockfile', VERSIONS_LOCK]
.map((sfx) => fileName.endsWith(sfx))
.some((v) => v);
};

async function getUpdatedLockfiles(
oldLockFileContentMap: Record<string, string | null>
): Promise<UpdateArtifactsResult[]> {
Expand All @@ -31,7 +42,7 @@ async function getUpdatedLockfiles(
const status = await getRepoStatus();

for (const modifiedFile of status.modified) {
if (modifiedFile.endsWith('.lockfile')) {
if (isLockFile(modifiedFile)) {
const newContent = await readLocalFile(modifiedFile, 'utf8');
if (oldLockFileContentMap[modifiedFile] !== newContent) {
res.push({
Expand Down Expand Up @@ -90,7 +101,7 @@ export async function updateArtifacts({
logger.debug(`gradle.updateArtifacts(${packageFileName})`);

const fileList = await getFileList();
const lockFiles = fileList.filter((file) => file.endsWith('.lockfile'));
const lockFiles = fileList.filter((file) => isLockFile(file));
if (!lockFiles.length) {
logger.debug('No Gradle dependency lockfiles found - skipping update');
return null;
Expand Down Expand Up @@ -147,7 +158,7 @@ export async function updateArtifacts({
.map(quote)
.join(' ')}`;

if (config.isLockFileMaintenance) {
if (config.isLockFileMaintenance || isGcvPropsFile(packageFileName)) {
cmd += ' --write-locks';
} else {
const updatedDepNames = updatedDeps
Expand Down
73 changes: 73 additions & 0 deletions lib/modules/manager/gradle/extract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,4 +841,77 @@ describe('modules/manager/gradle/extract', () => {
},
]);
});

it('gradle-consistent-versions plugin check enabled', async () => {
const buildFile = `
apply from: 'test.gradle'
repositories {
mavenCentral()
}
apply plugin: 'com.palantir.consistent-versions'
dependencies {
implementation "org.apache.lucene"
}
`;

const versionsProps = `org.apache.lucene:* = 1.2.3`;

const versionsLock = `
org.apache.lucene:lucene-core:1.2.3 (10 constraints: 95be0c15)
org.apache.lucene:lucene-codecs:1.2.3 (5 constraints: 1231231)
`;

mockFs({
'build.gradle': buildFile,
'versions.props': versionsProps,
'versions.lock': versionsLock,
});

const res = await extractAllPackageFiles({} as ExtractConfig, [
'build.gradle',
'versions.props',
'versions.lock',
]);

expect(res).toMatchObject([
{
packageFile: 'versions.lock',
},
{
packageFile: 'versions.props',
deps: [
{
depName: 'org.apache.lucene:lucene-core',
depType: 'dependencies',
fileReplacePosition: 22,
groupName: 'org.apache.lucene:-',
lockedVersion: '1.2.3',
managerData: {
fileReplacePosition: 22,
packageFile: 'versions.props',
},
registryUrls: ['https://repo.maven.apache.org/maven2'],
},
{
depName: 'org.apache.lucene:lucene-codecs',
depType: 'dependencies',
fileReplacePosition: 22,
groupName: 'org.apache.lucene:-',
lockedVersion: '1.2.3',
managerData: {
fileReplacePosition: 22,
packageFile: 'versions.props',
},
registryUrls: ['https://repo.maven.apache.org/maven2'],
},
],
},
{
packageFile: 'build.gradle',
},
]);
});
});
19 changes: 19 additions & 0 deletions lib/modules/manager/gradle/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import { getFileContentMap } from '../../../util/fs';
import { MavenDatasource } from '../../datasource/maven';
import type { ExtractConfig, PackageDependency, PackageFile } from '../types';
import { parseCatalog } from './extract/catalog';
import {
VERSIONS_LOCK,
VERSIONS_PROPS,
isGcvPropsFile,
parseGcv,
usesGcv,
} from './extract/consistentVersionsPlugin';
import { parseGradle, parseProps } from './parser';
import type {
GradleManagerData,
Expand Down Expand Up @@ -53,6 +60,9 @@ export async function extractAllPackageFiles(
deps: [],
};

// Check if gradle-consistent-versions plugin is in use by repo
const usesConsistentVersionPlugin = usesGcv(reorderedFiles, fileContents);

try {
// TODO #7154
const content = fileContents[packageFile]!;
Expand All @@ -70,6 +80,15 @@ export async function extractAllPackageFiles(
} else if (isTOMLFile(packageFile)) {
const updatesFromCatalog = parseCatalog(packageFile, content);
extractedDeps.push(...updatesFromCatalog);
} else if (isGcvPropsFile(packageFile)) {
if (usesConsistentVersionPlugin && packageFile === VERSIONS_PROPS) {
// We use gradle-consistent-versions, and pass in both the versions file and the lock file
const updatesFromGcv = parseGcv(
fileContents[VERSIONS_PROPS]!,
fileContents[VERSIONS_LOCK]!
);
extractedDeps.push(...updatesFromGcv);
}
} else {
const vars = getVars(registry, dir);
const {
Expand Down
182 changes: 182 additions & 0 deletions lib/modules/manager/gradle/extract/consistentVersionsPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { logger } from '../../../../logger';
import { newlineRegex, regEx } from '../../../../util/regex';
import type { PackageDependency } from '../../types';
import type { GradleManagerData } from '../types';

export const VERSIONS_PROPS = 'versions.props';
export const VERSIONS_LOCK = 'versions.lock';

/**
* Determines if Palantir Gradle consistent-versions is in use, https://github.com/palantir/gradle-consistent-versions.
* The plugin name must be in build file and both `versions.props` and `versions.lock` must exist.
*
* @param availableFiles list of gradle build files found in project
* @param fileContents map with file contents of all files
*/
export function usesGcv(
availableFiles: string[],
fileContents: Record<string, string | null>
): boolean {
if (
fileContents['build.gradle']?.includes(
'com.palantir.consistent-versions'
) ||
fileContents['build.kts']?.includes('com.palantir.consistent-versions')
) {
if (
availableFiles.includes(VERSIONS_PROPS) &&
availableFiles.includes(VERSIONS_LOCK)
) {
logger.debug('This repo uses gradle-consistent-versions');
return true;
}
}
return false;
}

/**
* Confirms whether the provided file name is one of the two GCV files
*/
export function isGcvPropsFile(fileName: string): boolean {
return fileName === VERSIONS_PROPS || fileName === VERSIONS_LOCK;
}

/**
* Parses Gradle-Consistent-Versions files to figure out what dependencies, versions
* and groups they contain. The parsing goes like this:
* - Parse `versions.props` into deps (or groups) and versions, remembering file offsets
* - Parse `versions.lock` into deps and lock-versions
* - For each exact dep in props file, lookup the lock-version from lock file
* - For each group/regex dep in props file, lookup the set of exact deps and versions in lock file
*
* @param propsFileContent text content of `versions.props
* @param lockFileContent text content of `versions.lock`
*/
export function parseGcv(
propsFileContent: string,
lockFileContent: string
): PackageDependency<GradleManagerData>[] {
const lockFileMap = parseLockFile(lockFileContent);
const [propsFileExactMap, propsFileRegexMap] =
parsePropsFile(propsFileContent);

const extractedDeps: PackageDependency<GradleManagerData>[] = [];

// For each exact dep in props file
for (const [propDep, versionAndPosition] of propsFileExactMap) {
if (lockFileMap.has(propDep)) {
const newDep: Record<string, any> = {
managerData: {
packageFile: VERSIONS_PROPS,
fileReplacePosition: versionAndPosition.filePos,
},
packageName: propDep,
currentValue: versionAndPosition.version,
currentVersion: versionAndPosition.version,
lockedVersion: lockFileMap.get(propDep),
} as PackageDependency<GradleManagerData>;
extractedDeps.push(newDep);
}
}

// For each regular expression (group) dep in props file
for (const [propDepRegEx, propVerAndPos] of propsFileRegexMap) {
for (const [exactDep, exactVer] of lockFileMap) {
if (propDepRegEx.test(exactDep)) {
const newDep: Record<string, any> = {
managerData: {
packageFile: VERSIONS_PROPS,
fileReplacePosition: propVerAndPos.filePos,
},
depName: exactDep,
currentValue: propVerAndPos.version,
currentVersion: propVerAndPos.version,
lockedVersion: exactVer,
groupName: regexToGroupString(propDepRegEx),
} as PackageDependency<GradleManagerData>;
extractedDeps.push(newDep);
}
}
}
return extractedDeps;
}

//----------------------------------
// Private utility functions below
//----------------------------------

// Translate GCV's glob syntax to regex
function globToRegex(depName: string): RegExp {
return regEx(depName.replaceAll('.', '\\.').replaceAll('*', '[^:]*'));
}

// Translate the regex from versions.props into a group name used in branch names etc
function regexToGroupString(regExp: RegExp): string {
return regExp.source.replaceAll('[^:]*', '-').replaceAll('\\.', '.');
}

interface VersionWithPosition {
version?: string;
filePos?: number;
}

/**
* Parses `versions.lock`
*/
function parseLockFile(input: string): Map<string, string> {
const lockLineRegex = regEx(
`^(?<depName>[^:]+:[^:]+):(?<lockVersion>[^ ]+) \\(\\d+ constraints: [0-9a-f]+\\)$`
);

const depVerMap = new Map<string, string>();
for (const line of input.split(newlineRegex)) {
const lineMatch = lockLineRegex.exec(line);
if (lineMatch?.groups) {
const { depName, lockVersion } = lineMatch.groups;
depVerMap.set(depName, lockVersion);
}
}
logger.trace(
`Found ${depVerMap.size} locked dependencies in ${VERSIONS_LOCK}.`
);
return depVerMap;
}

/**
* Parses `versions.props`
*/
function parsePropsFile(
input: string
): [Map<string, VersionWithPosition>, Map<RegExp, VersionWithPosition>] {
const propsLineRegex = regEx(
`^(?<depName>[^:]+:[^=]+?) *= *(?<propsVersion>.*)$`
);
const depVerExactMap = new Map<string, VersionWithPosition>();
const depVerRegexMap = new Map<RegExp, VersionWithPosition>();

let startOfLineIdx = 0;
for (const line of input.split(newlineRegex)) {
const lineMatch = propsLineRegex.exec(line);
if (lineMatch?.groups) {
const { depName, propsVersion } = lineMatch.groups;
const startPosInLine = line.lastIndexOf(propsVersion);
const propVersionPos = startOfLineIdx + startPosInLine;
if (depName.includes('*')) {
depVerRegexMap.set(globToRegex(depName), {
version: propsVersion,
filePos: propVersionPos,
});
} else {
depVerExactMap.set(depName, {
version: propsVersion,
filePos: propVersionPos,
});
}
}
startOfLineIdx += line.length + 1;
}
logger.trace(
`Found ${depVerExactMap.size} dependencies and ${depVerRegexMap.size} wildcard dependencies in ${VERSIONS_PROPS}.`
);
return [depVerExactMap, depVerRegexMap];
}
7 changes: 7 additions & 0 deletions lib/modules/manager/gradle/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { ProgrammingLanguage } from '../../../constants';
import { MavenDatasource } from '../../datasource/maven';
import * as gradleVersioning from '../../versioning/gradle';
import {
VERSIONS_LOCK,
VERSIONS_PROPS,
} from './extract/consistentVersionsPlugin';

export { extractAllPackageFiles } from './extract';
export { updateDependency } from './update';
Expand All @@ -15,6 +19,9 @@ export const defaultConfig = {
'(^|\\/)gradle\\.properties$',
'(^|\\/)gradle\\/.+\\.toml$',
'\\.versions\\.toml$',
// The two below is for gradle-consistent-versions plugin
`^${VERSIONS_PROPS}$`,
`^${VERSIONS_LOCK}$`,
],
timeout: 600,
versioning: gradleVersioning.id,
Expand Down

0 comments on commit 33b0ca9

Please sign in to comment.