mirror of https://github.com/renovatebot/renovate
214 lines
7.7 KiB
TypeScript
214 lines
7.7 KiB
TypeScript
import { logger } from '../../../../logger';
|
|
import * as fs from '../../../../util/fs';
|
|
import { newlineRegex, regEx } from '../../../../util/regex';
|
|
import { coerceString } from '../../../../util/string';
|
|
import type { PackageDependency } from '../../types';
|
|
import type { GradleManagerData } from '../types';
|
|
import { isDependencyString, versionLikeSubstring } from '../utils';
|
|
|
|
export const VERSIONS_PROPS = 'versions.props';
|
|
export const VERSIONS_LOCK = 'versions.lock';
|
|
export const LOCKFIlE_HEADER_TEXT = regEx(
|
|
/^# Run \.\/gradlew (?:--write-locks|writeVersionsLock|writeVersionsLocks) to regenerate this file/,
|
|
);
|
|
|
|
/**
|
|
* Determines if Palantir gradle-consistent-versions is in use, https://github.com/palantir/gradle-consistent-versions.
|
|
* Both `versions.props` and `versions.lock` must exist and the special header line of lock file must match
|
|
*
|
|
* @param versionsPropsFilename is the full file name path of `versions.props`
|
|
* @param fileContents map with file contents of all files
|
|
*/
|
|
export function usesGcv(
|
|
versionsPropsFilename: string,
|
|
fileContents: Record<string, string | null>,
|
|
): boolean {
|
|
const versionsLockFile: string = fs.getSiblingFileName(
|
|
versionsPropsFilename,
|
|
VERSIONS_LOCK,
|
|
);
|
|
|
|
return !!fileContents[versionsLockFile]?.match(LOCKFIlE_HEADER_TEXT);
|
|
}
|
|
|
|
/**
|
|
* Confirms whether the provided file name is the props file
|
|
*/
|
|
export function isGcvPropsFile(fileName: string): boolean {
|
|
return fileName === VERSIONS_PROPS || fileName.endsWith(`/${VERSIONS_PROPS}`);
|
|
}
|
|
|
|
/**
|
|
* Confirms whether the provided file name is the lock file
|
|
*/
|
|
export function isGcvLockFile(fileName: string): boolean {
|
|
return fileName === VERSIONS_LOCK || fileName.endsWith(`/${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 propsFileName name and path of the props file
|
|
* @param fileContents text content of all files
|
|
*/
|
|
export function parseGcv(
|
|
propsFileName: string,
|
|
fileContents: Record<string, string | null>,
|
|
): PackageDependency<GradleManagerData>[] {
|
|
const propsFileContent = coerceString(fileContents[propsFileName]);
|
|
const lockFileName = fs.getSiblingFileName(propsFileName, VERSIONS_LOCK);
|
|
const lockFileContent = coerceString(fileContents[lockFileName]);
|
|
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: propsFileName,
|
|
fileReplacePosition: versionAndPosition.filePos,
|
|
},
|
|
depName: propDep,
|
|
currentValue: versionAndPosition.version,
|
|
lockedVersion: lockFileMap.get(propDep)?.version,
|
|
depType: lockFileMap.get(propDep)?.depType,
|
|
} satisfies PackageDependency<GradleManagerData>;
|
|
extractedDeps.push(newDep);
|
|
// Remove from the lockfile map so the same exact lib will not be included in globbing
|
|
lockFileMap.delete(propDep);
|
|
}
|
|
}
|
|
|
|
// For each regular expression dep in props file (starting with the longest glob string)...
|
|
for (const [propDepGlob, propVerAndPos] of propsFileRegexMap) {
|
|
const globRegex = globToRegex(propDepGlob);
|
|
for (const [exactDep, lockVersionAndDepType] of lockFileMap) {
|
|
if (globRegex.test(exactDep)) {
|
|
const newDep: Record<string, any> = {
|
|
managerData: {
|
|
packageFile: propsFileName,
|
|
fileReplacePosition: propVerAndPos.filePos,
|
|
},
|
|
depName: exactDep,
|
|
currentValue: propVerAndPos.version,
|
|
lockedVersion: lockVersionAndDepType.version,
|
|
depType: lockVersionAndDepType.depType,
|
|
groupName: propDepGlob,
|
|
} satisfies PackageDependency<GradleManagerData>;
|
|
extractedDeps.push(newDep);
|
|
// Remove from the lockfile map so the same lib will not be included in more generic globs later
|
|
lockFileMap.delete(exactDep);
|
|
}
|
|
}
|
|
}
|
|
return extractedDeps;
|
|
}
|
|
|
|
// Translate glob syntax to a regex that does the same. Note that we cannot use replaceAll as it does not exist in Node14
|
|
// Loosely borrowed mapping to regex from https://github.com/palantir/gradle-consistent-versions/blob/develop/src/main/java/com/palantir/gradle/versions/FuzzyPatternResolver.java
|
|
function globToRegex(depName: string): RegExp {
|
|
return regEx(
|
|
depName
|
|
.replace(/\*/g, '_WC_CHAR_')
|
|
.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&')
|
|
.replace(/_WC_CHAR_/g, '.*?'),
|
|
);
|
|
}
|
|
|
|
interface VersionWithPosition {
|
|
version: string;
|
|
filePos: number;
|
|
}
|
|
|
|
interface VersionWithDepType {
|
|
version: string;
|
|
depType: string;
|
|
}
|
|
|
|
/**
|
|
* Parses `versions.lock`
|
|
*/
|
|
export function parseLockFile(input: string): Map<string, VersionWithDepType> {
|
|
const lockLineRegex = regEx(
|
|
`^(?<depName>[^:]+:[^:]+):(?<lockVersion>[^ ]+) \\(\\d+ constraints: [0-9a-f]+\\)$`,
|
|
);
|
|
|
|
const depVerMap = new Map<string, VersionWithDepType>();
|
|
let isTestDepType = false;
|
|
for (const line of input.split(newlineRegex)) {
|
|
const lineMatch = lockLineRegex.exec(line);
|
|
if (lineMatch?.groups) {
|
|
const { depName, lockVersion } = lineMatch.groups;
|
|
if (isDependencyString(`${depName}:${lockVersion}`)) {
|
|
depVerMap.set(depName, {
|
|
version: lockVersion,
|
|
depType: isTestDepType ? 'test' : 'dependencies',
|
|
} as VersionWithDepType);
|
|
}
|
|
} else if (line === '[Test dependencies]') {
|
|
isTestDepType = true; // We know that all lines below this header are test dependencies
|
|
}
|
|
}
|
|
logger.trace(
|
|
`Found ${depVerMap.size} locked dependencies in ${VERSIONS_LOCK}.`,
|
|
);
|
|
return depVerMap;
|
|
}
|
|
|
|
/**
|
|
* Parses `versions.props`, this is CR/LF safe
|
|
* @param input the entire property file from file system
|
|
* @return two maps, first being exact matches, second regex matches
|
|
*/
|
|
export function parsePropsFile(
|
|
input: string,
|
|
): [Map<string, VersionWithPosition>, Map<string, VersionWithPosition>] {
|
|
const propsLineRegex = regEx(
|
|
`^(?<depName>[^:]+:[^=]+?) *= *(?<propsVersion>.*)$`,
|
|
);
|
|
const depVerExactMap = new Map<string, VersionWithPosition>();
|
|
const depVerRegexMap = new Map<string, VersionWithPosition>();
|
|
|
|
let startOfLineIdx = 0;
|
|
const isCrLf = input.indexOf('\r\n') > 0;
|
|
const validGlob = /^[a-zA-Z][-_a-zA-Z0-9.:*]+$/;
|
|
for (const line of input.split(newlineRegex)) {
|
|
const lineMatch = propsLineRegex.exec(line);
|
|
if (lineMatch?.groups) {
|
|
const { depName, propsVersion } = lineMatch.groups;
|
|
if (
|
|
validGlob.test(depName) &&
|
|
versionLikeSubstring(propsVersion) !== null
|
|
) {
|
|
const startPosInLine = line.lastIndexOf(propsVersion);
|
|
const propVersionPos = startOfLineIdx + startPosInLine;
|
|
if (depName.includes('*')) {
|
|
depVerRegexMap.set(depName, {
|
|
version: propsVersion,
|
|
filePos: propVersionPos,
|
|
});
|
|
} else {
|
|
depVerExactMap.set(depName, {
|
|
version: propsVersion,
|
|
filePos: propVersionPos,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
startOfLineIdx += line.length + (isCrLf ? 2 : 1);
|
|
}
|
|
logger.trace(
|
|
`Found ${depVerExactMap.size} dependencies and ${depVerRegexMap.size} wildcard dependencies in ${VERSIONS_PROPS}.`,
|
|
);
|
|
return [depVerExactMap, new Map([...depVerRegexMap].sort().reverse())];
|
|
}
|