import { isNonEmptyStringAndNotWhitespace } from '@sindresorhus/is'; import { quote } from 'shlex'; import upath from 'upath'; import { GlobalConfig } from '../../../config/global.ts'; import { TEMPORARY_ERROR } from '../../../constants/error-messages.ts'; import { logger } from '../../../logger/index.ts'; import { exec, getToolSettingsOptions, gradleJvmArg, } from '../../../util/exec/index.ts'; import type { ExecOptions } from '../../../util/exec/types.ts'; import { findUpLocal, readLocalFile, writeLocalFile, } from '../../../util/fs/index.ts'; import { getFiles, getRepoStatus } from '../../../util/git/index.ts'; import { regEx } from '../../../util/regex.ts'; import { scm } from '../../platform/scm.ts'; import { extraEnv, extractGradleVersion, getJavaConstraint, gradleWrapperFileName, prepareGradleCommand, } from '../gradle-wrapper/utils.ts'; import type { UpdateArtifact, UpdateArtifactsResult } from '../types.ts'; import { isGcvLockFile, isGcvPropsFile, } from './extract/consistent-versions-plugin.ts'; import { isGradleBuildFile } from './utils.ts'; export function isGradleExecutionAllowed(command: string): boolean { const allowlist = GlobalConfig.get('allowedUnsafeExecutions'); if (!allowlist.includes('gradleWrapper')) { logger.once.warn( { command }, 'Gradle wrapper command was requested to run, but `gradleWrapper` is not permitted in the allowedUnsafeExecutions', ); return false; } return true; } // .lockfile is gradle default lockfile, /versions.lock is gradle-consistent-versions plugin lockfile function isLockFile(fileName: string): boolean { return fileName.endsWith('.lockfile') || isGcvLockFile(fileName); } async function getUpdatedLockfiles( oldLockFileContentMap: Record, ): Promise { const res: UpdateArtifactsResult[] = []; const status = await getRepoStatus(); for (const modifiedFile of status.modified) { if ( isLockFile(modifiedFile) || modifiedFile.endsWith('gradle/verification-metadata.xml') ) { const newContent = await readLocalFile(modifiedFile, 'utf8'); if (oldLockFileContentMap[modifiedFile] !== newContent) { res.push({ file: { type: 'addition', path: modifiedFile, contents: newContent, }, }); } } } return res; } async function getSubProjectList( cmd: string, execOptions: ExecOptions, ): Promise { const subprojects = ['']; // = root project const subprojectsRegex = regEx(/^[ \t]*subprojects: \[(?.+)\]/m); const gradleProperties = await exec(`${cmd} properties`, execOptions); const subprojectsMatch = gradleProperties.stdout.match(subprojectsRegex); if (subprojectsMatch?.groups?.subprojects) { const projectRegex = regEx(/project '(?.+?)'/g); const matches = subprojectsMatch.groups.subprojects.matchAll(projectRegex); for (const match of matches) { if (match?.groups?.name) { subprojects.push(match.groups.name); } } } return subprojects; } async function getGradleVersion(gradlewFile: string): Promise { const propertiesFile = upath.join( upath.dirname(gradlewFile), 'gradle/wrapper/gradle-wrapper.properties', ); const properties = await readLocalFile(propertiesFile, 'utf8'); const extractResult = extractGradleVersion(properties ?? ''); return extractResult ? extractResult.version : null; } async function buildUpdateVerificationMetadataCmd( verificationMetadataFile: string | undefined, baseCmd: string, ): Promise { if (!verificationMetadataFile) { return null; } const verificationMetadata = await readLocalFile(verificationMetadataFile); const verifiesChecksums = verificationMetadata?.includes( 'true', ); const verifiesSignatures = verificationMetadata?.includes( 'true', ); const hashTypes = ['sha256', 'sha512'].filter((type) => verificationMetadata?.includes(`<${type} `), ); if (verifiesChecksums || hashTypes.length) { logger.debug( 'Dependency metadata verification enabled or checksums present - generating checksums', ); } if ((verifiesChecksums || verifiesSignatures) && !hashTypes.length) { // fallback algorithm for pgp and in case a weak algorithm (md5, sha1) is used for checksums hashTypes.push('sha256'); } if (verifiesSignatures) { logger.debug( 'Dependency signature verification enabled - generating PGP signatures', ); hashTypes.push('pgp'); } if (!hashTypes.length) { return null; } return `${baseCmd} --write-verification-metadata ${hashTypes.join(',')} dependencies`; } export async function updateArtifacts({ packageFileName, updatedDeps, newPackageFileContent, config, }: UpdateArtifact): Promise { logger.debug(`gradle.updateArtifacts(${packageFileName})`); const fileList = await scm.getFileList(); const lockFiles = fileList.filter((file) => isLockFile(file)); const verificationMetadataFile = fileList.find((fileName) => fileName.endsWith('gradle/verification-metadata.xml'), ); if (!lockFiles.length && !verificationMetadataFile) { logger.debug( 'No Gradle dependency lockfiles or verification metadata found - skipping update', ); return null; } const gradlewName = gradleWrapperFileName(); const gradlewFile = await findUpLocal( gradlewName, upath.dirname(packageFileName), ); if (!gradlewFile) { logger.debug( 'Found Gradle dependency lockfiles but no gradlew - aborting update', ); return null; } if ( config.isLockFileMaintenance && (!isGradleBuildFile(packageFileName) || upath.dirname(packageFileName) !== upath.dirname(gradlewFile)) ) { logger.trace( 'No build.gradle(.kts) file or not in root project - skipping lock file maintenance', ); return null; } if (!isGradleExecutionAllowed(gradlewFile)) { logger.trace( 'Not allowed to execute gradle due to allowedUnsafeExecutions - aborting update', ); return null; } logger.debug('Updating found Gradle dependency lockfiles'); try { const oldLockFileContentMap = await getFiles(lockFiles); await prepareGradleCommand(gradlewFile); const baseCmd = `${gradlewName}${gradleJvmArg(getToolSettingsOptions(config.toolSettings))} --console=plain --dependency-verification lenient -q`; const execOptions: ExecOptions = { cwdFile: gradlewFile, docker: {}, extraEnv, toolConstraints: [ { toolName: 'java', constraint: config.constraints?.java ?? (await getJavaConstraint( await getGradleVersion(gradlewFile), gradlewFile, )), }, ], }; const cmds = []; if (lockFiles.length) { const subprojects = await getSubProjectList(baseCmd, execOptions); let lockfileCmd = `${baseCmd} ${subprojects .map((project) => `${project}:dependencies`) .map(quote) .join(' ')}`; if ( config.isLockFileMaintenance === true || !updatedDeps.length || isGcvPropsFile(packageFileName) ) { lockfileCmd += ' --write-locks'; } else { const updatedDepNames = updatedDeps .map(({ depName, packageName }) => packageName ?? depName) .filter(isNonEmptyStringAndNotWhitespace); lockfileCmd += ` --update-locks ${updatedDepNames .map(quote) .join(',')}`; } cmds.push(lockfileCmd); } const updateVerificationMetadataCmd = await buildUpdateVerificationMetadataCmd( verificationMetadataFile, baseCmd, ); if (updateVerificationMetadataCmd) { cmds.push(updateVerificationMetadataCmd); } if (!cmds.length) { logger.debug('No lockfile or verification metadata update necessary'); return null; } await writeLocalFile(packageFileName, newPackageFileContent); await exec(cmds, { ...execOptions, ignoreStdout: true }); const res = await getUpdatedLockfiles(oldLockFileContentMap); logger.debug('Returning updated Gradle dependency lockfiles'); return res.length > 0 ? res : null; } catch (err) { if (err.message === TEMPORARY_ERROR) { throw err; } logger.debug({ err }, 'Error while updating Gradle dependency lockfiles'); return [ { artifactError: { fileName: packageFileName, stderr: err.message, }, }, ]; } }