From ae0ac14ca1d5183cac8da07fa160078ba23fe644 Mon Sep 17 00:00:00 2001 From: Michael Kriese Date: Thu, 19 Aug 2021 14:44:33 +0200 Subject: [PATCH] feat(gradle): use java image for docker mode (#11316) --- .../gradle-wrapper/artifacts-real.spec.ts | 18 +- lib/manager/gradle-wrapper/artifacts.spec.ts | 20 +- lib/manager/gradle-wrapper/artifacts.ts | 16 +- lib/manager/gradle-wrapper/extract.ts | 34 ++- lib/manager/gradle-wrapper/util.spec.ts | 36 ++++ lib/manager/gradle-wrapper/utils.ts | 91 ++++++++ .../deep/__snapshots__/index.spec.ts.snap | 18 +- .../gradle/deep/gradle-updates-report.spec.ts | 26 ++- .../gradle/deep/gradle-updates-report.ts | 16 +- lib/manager/gradle/deep/index.spec.ts | 204 ++++++++++++++---- lib/manager/gradle/deep/index.ts | 35 +-- lib/manager/gradle/deep/utils.ts | 74 ++++--- lib/manager/types.ts | 1 + 13 files changed, 426 insertions(+), 163 deletions(-) create mode 100644 lib/manager/gradle-wrapper/util.spec.ts create mode 100644 lib/manager/gradle-wrapper/utils.ts diff --git a/lib/manager/gradle-wrapper/artifacts-real.spec.ts b/lib/manager/gradle-wrapper/artifacts-real.spec.ts index fa522df6501390..acb1e1debbd9ee 100644 --- a/lib/manager/gradle-wrapper/artifacts-real.spec.ts +++ b/lib/manager/gradle-wrapper/artifacts-real.spec.ts @@ -8,7 +8,7 @@ import type { RepoGlobalConfig } from '../../config/types'; import type { StatusResult } from '../../util/git'; import { ifSystemSupportsGradle } from '../gradle/deep/__testutil__/gradle'; import type { UpdateArtifactsConfig } from '../types'; -import * as dcUpdate from '.'; +import * as gradleWrapper from '.'; jest.mock('../../util/git'); @@ -60,7 +60,7 @@ describe('manager/gradle-wrapper/artifacts-real', () => { ], } as StatusResult); - const res = await dcUpdate.updateArtifacts({ + const res = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: await readString( @@ -100,7 +100,7 @@ describe('manager/gradle-wrapper/artifacts-real', () => { }) ); - const result = await dcUpdate.updateArtifacts({ + const result = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: ``, @@ -118,7 +118,7 @@ describe('manager/gradle-wrapper/artifacts-real', () => { modified: [], } as StatusResult); - const res = await dcUpdate.updateArtifacts({ + const res = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: await readString( @@ -142,7 +142,7 @@ describe('manager/gradle-wrapper/artifacts-real', () => { throw new Error('failed'); }); - const res = await dcUpdate.updateArtifacts({ + const res = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: await readString( @@ -169,7 +169,7 @@ describe('manager/gradle-wrapper/artifacts-real', () => { }; setGlobalConfig(wrongCmdConfig); - const res = await dcUpdate.updateArtifacts({ + const res = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: await readString( @@ -192,7 +192,7 @@ describe('manager/gradle-wrapper/artifacts-real', () => { it('gradlew not found', async () => { setGlobalConfig({ localDir: 'some-dir' }); - const res = await dcUpdate.updateArtifacts({ + const res = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: undefined, @@ -219,7 +219,7 @@ describe('manager/gradle-wrapper/artifacts-real', () => { const newContent = await readString(`./gradle-wrapper-sum.properties`); - const result = await dcUpdate.updateArtifacts({ + const result = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: newContent.replace( @@ -263,7 +263,7 @@ describe('manager/gradle-wrapper/artifacts-real', () => { .get('/distributions/gradle-6.3-bin.zip.sha256') .reply(404); - const result = await dcUpdate.updateArtifacts({ + const result = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`, diff --git a/lib/manager/gradle-wrapper/artifacts.spec.ts b/lib/manager/gradle-wrapper/artifacts.spec.ts index 5ce870f9dcc834..bcd2199fa5f731 100644 --- a/lib/manager/gradle-wrapper/artifacts.spec.ts +++ b/lib/manager/gradle-wrapper/artifacts.spec.ts @@ -1,8 +1,6 @@ -/* eslint jest/no-standalone-expect: 0 */ -import { exec as _exec } from 'child_process'; -import { readFile } from 'fs-extra'; +import { readFile, stat } from 'fs-extra'; import { resolve } from 'upath'; -import { envMock, mockExecAll } from '../../../test/exec-util'; +import { envMock, exec, mockExecAll } from '../../../test/exec-util'; import * as httpMock from '../../../test/http-mock'; import { addReplacingSerializer, @@ -16,14 +14,13 @@ import type { RepoGlobalConfig } from '../../config/types'; import { resetPrefetchedImages } from '../../util/exec/docker'; import type { StatusResult } from '../../util/git'; import type { UpdateArtifactsConfig } from '../types'; -import * as dcUpdate from '.'; +import * as gradleWrapper from '.'; jest.mock('child_process'); jest.mock('../../util/fs'); jest.mock('../../util/git'); jest.mock('../../util/exec/env'); -const exec: jest.Mock = _exec as any; const fixtures = resolve(__dirname, './__fixtures__'); const adminConfig: RepoGlobalConfig = { @@ -57,6 +54,7 @@ describe('manager/gradle-wrapper/artifacts', () => { resetPrefetchedImages(); fs.readLocalFile.mockResolvedValue('test'); + fs.stat.mockImplementation((p) => stat(p)); }); afterEach(() => { @@ -74,7 +72,7 @@ describe('manager/gradle-wrapper/artifacts', () => { const execSnapshots = mockExecAll(exec); - const res = await dcUpdate.updateArtifacts({ + const res = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle/wrapper/gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: await readString( @@ -100,7 +98,7 @@ describe('manager/gradle-wrapper/artifacts', () => { it('gradlew not found', async () => { setGlobalConfig({ ...adminConfig, localDir: 'some-dir' }); - const res = await dcUpdate.updateArtifacts({ + const res = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: undefined, @@ -117,7 +115,7 @@ describe('manager/gradle-wrapper/artifacts', () => { modified: [], }) ); - const res = await dcUpdate.updateArtifacts({ + const res = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: '', @@ -145,7 +143,7 @@ describe('manager/gradle-wrapper/artifacts', () => { const execSnapshots = mockExecAll(exec); - const result = await dcUpdate.updateArtifacts({ + const result = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`, @@ -179,7 +177,7 @@ describe('manager/gradle-wrapper/artifacts', () => { .get('/distributions/gradle-6.3-bin.zip.sha256') .reply(404); - const result = await dcUpdate.updateArtifacts({ + const result = await gradleWrapper.updateArtifacts({ packageFileName: 'gradle-wrapper.properties', updatedDeps: [], newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`, diff --git a/lib/manager/gradle-wrapper/artifacts.ts b/lib/manager/gradle-wrapper/artifacts.ts index 9e551d7ec4e83a..d594c16ad8e278 100644 --- a/lib/manager/gradle-wrapper/artifacts.ts +++ b/lib/manager/gradle-wrapper/artifacts.ts @@ -1,18 +1,19 @@ -import { stat } from 'fs-extra'; import { resolve } from 'upath'; import { getGlobalConfig } from '../../config/global'; import { TEMPORARY_ERROR } from '../../constants/error-messages'; import { logger } from '../../logger'; import { ExecOptions, exec } from '../../util/exec'; -import { readLocalFile, writeLocalFile } from '../../util/fs'; +import { readLocalFile, stat, writeLocalFile } from '../../util/fs'; import { StatusResult, getRepoStatus } from '../../util/git'; import { Http } from '../../util/http'; +import type { UpdateArtifact, UpdateArtifactsResult } from '../types'; import { extraEnv, + getJavaContraint, + getJavaVersioning, gradleWrapperFileName, prepareGradleCommand, -} from '../gradle/deep/utils'; -import type { UpdateArtifact, UpdateArtifactsResult } from '../types'; +} from './utils'; const http = new Http('gradle-wrapper'); @@ -57,7 +58,7 @@ export async function updateArtifacts({ try { const { localDir: projectDir } = getGlobalConfig(); logger.debug({ updatedDeps }, 'gradle-wrapper.updateArtifacts()'); - const gradlew = gradleWrapperFileName(config); + const gradlew = gradleWrapperFileName(); const gradlewPath = resolve(projectDir, `./${gradlew}`); let cmd = await prepareGradleCommand( gradlew, @@ -87,7 +88,10 @@ export async function updateArtifacts({ logger.debug(`Updating gradle wrapper: "${cmd}"`); const execOptions: ExecOptions = { docker: { - image: 'gradle', + image: 'java', + tagConstraint: + config.constraints?.java ?? getJavaContraint(config.currentValue), + tagScheme: getJavaVersioning(), }, extraEnv, }; diff --git a/lib/manager/gradle-wrapper/extract.ts b/lib/manager/gradle-wrapper/extract.ts index 8bc09791875026..88383865230e98 100644 --- a/lib/manager/gradle-wrapper/extract.ts +++ b/lib/manager/gradle-wrapper/extract.ts @@ -1,30 +1,20 @@ import { GradleVersionDatasource } from '../../datasource/gradle-version'; import { logger } from '../../logger'; -import { regEx } from '../../util/regex'; -import * as gradleVersioning from '../../versioning/gradle'; +import { id as versioning } from '../../versioning/gradle'; import type { PackageDependency, PackageFile } from '../types'; - -// https://regex101.com/r/1GaQ2X/1 -const DISTRIBUTION_URL_REGEX = regEx( - '^(?:distributionUrl\\s*=\\s*)\\S*-(?\\d+\\.\\d+(?:\\.\\d+)?(?:-\\w+)*)-(?bin|all)\\.zip\\s*$' -); +import { extractGradleVersion } from './utils'; export function extractPackageFile(fileContent: string): PackageFile | null { - logger.debug('gradle-wrapper.extractPackageFile()'); - const lines = fileContent.split('\n'); - - for (const line of lines) { - const distributionUrlMatch = DISTRIBUTION_URL_REGEX.exec(line); - if (distributionUrlMatch) { - const dependency: PackageDependency = { - depName: 'gradle', - currentValue: distributionUrlMatch.groups.version, - datasource: GradleVersionDatasource.id, - versioning: gradleVersioning.id, - }; - logger.debug(dependency, 'Gradle Wrapper'); - return { deps: [dependency] }; - } + logger.trace('gradle-wrapper.extractPackageFile()'); + const currentValue = extractGradleVersion(fileContent); + if (currentValue) { + const dependency: PackageDependency = { + depName: 'gradle', + currentValue, + datasource: GradleVersionDatasource.id, + versioning, + }; + return { deps: [dependency] }; } return null; } diff --git a/lib/manager/gradle-wrapper/util.spec.ts b/lib/manager/gradle-wrapper/util.spec.ts new file mode 100644 index 00000000000000..9a65c9c4b9c5d2 --- /dev/null +++ b/lib/manager/gradle-wrapper/util.spec.ts @@ -0,0 +1,36 @@ +import { setGlobalConfig } from '../../config/global'; +import { extractGradleVersion, getJavaContraint } from './utils'; + +describe('manager/gradle-wrapper/util', () => { + describe('getJavaContraint()', () => { + it('return null for global mode', () => { + expect(getJavaContraint(undefined)).toBeNull(); + }); + + it('return ^11.0.0 for docker mode and undefined gradle', () => { + setGlobalConfig({ binarySource: 'docker' }); + expect(getJavaContraint(undefined)).toEqual('^11.0.0'); + }); + + it('return ^8.0.0 for docker gradle < 5', () => { + setGlobalConfig({ binarySource: 'docker' }); + expect(getJavaContraint('4.9')).toEqual('^8.0.0'); + }); + + it('return ^11.0.0 for docker gradle >=5 && <7', () => { + setGlobalConfig({ binarySource: 'docker' }); + expect(getJavaContraint('6.0')).toEqual('^11.0.0'); + }); + + it('return ^16.0.0 for docker gradle >= 7', () => { + setGlobalConfig({ binarySource: 'docker' }); + expect(getJavaContraint('7.0.1')).toEqual('^16.0.0'); + }); + }); + + describe('extractGradleVersion()', () => { + it('works for undefined', () => { + expect(extractGradleVersion(undefined)).toBeNull(); + }); + }); +}); diff --git a/lib/manager/gradle-wrapper/utils.ts b/lib/manager/gradle-wrapper/utils.ts new file mode 100644 index 00000000000000..f328ed53e9a6f6 --- /dev/null +++ b/lib/manager/gradle-wrapper/utils.ts @@ -0,0 +1,91 @@ +import type { Stats } from 'fs'; +import os from 'os'; +import upath from 'upath'; +import { getGlobalConfig } from '../../config/global'; +import { chmod } from '../../util/fs'; +import { regEx } from '../../util/regex'; +import gradleVersioning from '../../versioning/gradle'; +import { id as npmVersioning } from '../../versioning/npm'; + +export const extraEnv = { + GRADLE_OPTS: + '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.caching=false', +}; + +export function gradleWrapperFileName(): string { + if ( + os.platform() === 'win32' && + getGlobalConfig()?.binarySource !== 'docker' + ) { + return 'gradlew.bat'; + } + return './gradlew'; +} + +export async function prepareGradleCommand( + gradlewName: string, + cwd: string, + gradlew: Stats | null, + args: string | null +): Promise { + /* eslint-disable no-bitwise */ + // istanbul ignore if + if (gradlew?.isFile() === true) { + // if the file is not executable by others + if ((gradlew.mode & 0o1) === 0) { + // add the execution permission to the owner, group and others + await chmod(upath.join(cwd, gradlewName), gradlew.mode | 0o111); + } + if (args === null) { + return gradlewName; + } + return `${gradlewName} ${args}`; + } + /* eslint-enable no-bitwise */ + return null; +} + +/** + * Find compatible java version for gradle. + * see https://docs.gradle.org/current/userguide/compatibility.html + * @param gradleVersion current gradle version + * @returns A Java semver range + */ +export function getJavaContraint(gradleVersion: string): string | null { + if (getGlobalConfig()?.binarySource !== 'docker') { + // ignore + return null; + } + + const major = gradleVersioning.getMajor(gradleVersion); + if (major >= 7) { + return '^16.0.0'; + } + // first public gradle version was 2.0 + if (major > 0 && major < 5) { + return '^8.0.0'; + } + return '^11.0.0'; +} + +export function getJavaVersioning(): string { + return npmVersioning; +} + +// https://regex101.com/r/1GaQ2X/1 +const DISTRIBUTION_URL_REGEX = regEx( + '^(?:distributionUrl\\s*=\\s*)\\S*-(?\\d+\\.\\d+(?:\\.\\d+)?(?:-\\w+)*)-(?bin|all)\\.zip\\s*$' +); + +export function extractGradleVersion(fileContent: string): string | null { + const lines = fileContent?.split('\n') ?? []; + + for (const line of lines) { + const distributionUrlMatch = DISTRIBUTION_URL_REGEX.exec(line); + if (distributionUrlMatch) { + return distributionUrlMatch.groups.version; + } + } + + return null; +} diff --git a/lib/manager/gradle/deep/__snapshots__/index.spec.ts.snap b/lib/manager/gradle/deep/__snapshots__/index.spec.ts.snap index b5a158ef4ad61c..175a47f608ffe2 100644 --- a/lib/manager/gradle/deep/__snapshots__/index.spec.ts.snap +++ b/lib/manager/gradle/deep/__snapshots__/index.spec.ts.snap @@ -870,13 +870,13 @@ Array [ exports[`manager/gradle/deep/index extractPackageFile should use docker even if gradlew is available 2`] = ` Array [ Object { - "cmd": "docker pull renovate/gradle", + "cmd": "docker pull renovate/java:11.0.12", "options": Object { "encoding": "utf-8", }, }, Object { - "cmd": "docker ps --filter name=renovate_gradle -aq", + "cmd": "docker ps --filter name=renovate_java -aq", "options": Object { "encoding": "utf-8", }, @@ -888,7 +888,7 @@ Array [ }, }, Object { - "cmd": "docker run --rm --name=renovate_gradle --label=renovate_child -v \\"/foo/bar\\":\\"/foo/bar\\" -e GRADLE_OPTS -w \\"/foo/bar\\" renovate/gradle bash -l -c \\" --init-script renovate-plugin.gradle renovate\\"", + "cmd": "docker run --rm --name=renovate_java --label=renovate_child -v \\"/foo/bar\\":\\"/foo/bar\\" -e GRADLE_OPTS -w \\"/foo/bar\\" renovate/java:11.0.12 bash -l -c \\" --init-script renovate-plugin.gradle renovate\\"", "options": Object { "cwd": "/foo/bar", "encoding": "utf-8", @@ -1005,13 +1005,13 @@ Array [ exports[`manager/gradle/deep/index extractPackageFile should use docker even if gradlew.bat is available on Windows 2`] = ` Array [ Object { - "cmd": "docker pull renovate/gradle", + "cmd": "docker pull renovate/java:11.0.12", "options": Object { "encoding": "utf-8", }, }, Object { - "cmd": "docker ps --filter name=renovate_gradle -aq", + "cmd": "docker ps --filter name=renovate_java -aq", "options": Object { "encoding": "utf-8", }, @@ -1023,7 +1023,7 @@ Array [ }, }, Object { - "cmd": "docker run --rm --name=renovate_gradle --label=renovate_child -v \\"/foo/bar\\":\\"/foo/bar\\" -e GRADLE_OPTS -w \\"/foo/bar\\" renovate/gradle bash -l -c \\" --init-script renovate-plugin.gradle renovate\\"", + "cmd": "docker run --rm --name=renovate_java --label=renovate_child -v \\"/foo/bar\\":\\"/foo/bar\\" -e GRADLE_OPTS -w \\"/foo/bar\\" renovate/java:11.0.12 bash -l -c \\" --init-script renovate-plugin.gradle renovate\\"", "options": Object { "cwd": "/foo/bar", "encoding": "utf-8", @@ -1140,13 +1140,13 @@ Array [ exports[`manager/gradle/deep/index extractPackageFile should use docker if required 2`] = ` Array [ Object { - "cmd": "docker pull renovate/gradle", + "cmd": "docker pull renovate/java:11.0.12", "options": Object { "encoding": "utf-8", }, }, Object { - "cmd": "docker ps --filter name=renovate_gradle -aq", + "cmd": "docker ps --filter name=renovate_java -aq", "options": Object { "encoding": "utf-8", }, @@ -1158,7 +1158,7 @@ Array [ }, }, Object { - "cmd": "docker run --rm --name=renovate_gradle --label=renovate_child -v \\"/foo/bar\\":\\"/foo/bar\\" -e GRADLE_OPTS -w \\"/foo/bar\\" renovate/gradle bash -l -c \\"gradle --init-script renovate-plugin.gradle renovate\\"", + "cmd": "docker run --rm --name=renovate_java --label=renovate_child -v \\"/foo/bar\\":\\"/foo/bar\\" -e GRADLE_OPTS -w \\"/foo/bar\\" renovate/java:11.0.12 bash -l -c \\"install-tool gradle latest && gradle --init-script renovate-plugin.gradle renovate\\"", "options": Object { "cwd": "/foo/bar", "encoding": "utf-8", diff --git a/lib/manager/gradle/deep/gradle-updates-report.spec.ts b/lib/manager/gradle/deep/gradle-updates-report.spec.ts index 45a58fd4f7b42b..fc804f22cafb0b 100644 --- a/lib/manager/gradle/deep/gradle-updates-report.spec.ts +++ b/lib/manager/gradle/deep/gradle-updates-report.spec.ts @@ -1,13 +1,14 @@ import * as fs from 'fs-extra'; import tmp, { DirectoryResult } from 'tmp-promise'; import * as upath from 'upath'; +import { setGlobalConfig } from '../../../config/global'; import { exec } from '../../../util/exec'; +import { extraEnv } from '../../gradle-wrapper/utils'; import { ifSystemSupportsGradle } from './__testutil__/gradle'; import { GRADLE_DEPENDENCY_REPORT_FILENAME, createRenovateGradlePlugin, } from './gradle-updates-report'; -import { extraEnv } from './utils'; import { GRADLE_DEPENDENCY_REPORT_OPTIONS } from '.'; const fixtures = 'lib/manager/gradle/deep/__fixtures__'; @@ -21,27 +22,44 @@ describe('manager/gradle/deep/gradle-updates-report', () => { beforeEach(async () => { workingDir = await tmp.dir({ unsafeCleanup: true }); + setGlobalConfig({ localDir: workingDir.path }); }); + afterEach(() => workingDir.cleanup()); + it(`generates a report for Gradle version ${gradleVersion}`, async () => { await fs.copy(`${fixtures}/minimal-project`, workingDir.path); await fs.copy( `${fixtures}/gradle-wrappers/${gradleVersion}`, workingDir.path ); - await createRenovateGradlePlugin(workingDir.path); + await createRenovateGradlePlugin(); const gradlew = upath.join(workingDir.path, 'gradlew'); await exec(`${gradlew} ${GRADLE_DEPENDENCY_REPORT_OPTIONS}`, { cwd: workingDir.path, extraEnv, }); - // FIXME: explicit assert condition expect( fs.readJSONSync( `${workingDir.path}/${GRADLE_DEPENDENCY_REPORT_FILENAME}` ) - ).toMatchSnapshot(); + ).toMatchSnapshot([ + { + dependencies: [ + { + group: 'org.apache.commons', + name: 'commons-collections4', + version: '4.4', + }, + ], + project: 'minimal-test', + repositories: [ + 'https://jcenter.bintray.com/', + 'https://plugins.gradle.org/m2', + ], + }, + ]); }, 120000); } ); diff --git a/lib/manager/gradle/deep/gradle-updates-report.ts b/lib/manager/gradle/deep/gradle-updates-report.ts index cbaf6c4020fdc0..917caf1e675ffa 100644 --- a/lib/manager/gradle/deep/gradle-updates-report.ts +++ b/lib/manager/gradle/deep/gradle-updates-report.ts @@ -1,7 +1,11 @@ -import { exists, readFile, writeFile } from 'fs-extra'; import { join } from 'upath'; import * as datasourceSbtPackage from '../../../datasource/sbt-package'; import { logger } from '../../../logger'; +import { + localPathExists, + readLocalFile, + writeLocalFile, +} from '../../../util/fs'; import type { BuildDependency, GradleDependencyWithRepos, @@ -11,7 +15,7 @@ import type { export const GRADLE_DEPENDENCY_REPORT_FILENAME = 'gradle-renovate-report.json'; export async function createRenovateGradlePlugin( - localDir: string + gradleRoot = '.' ): Promise { const content = ` import groovy.json.JsonOutput @@ -47,11 +51,11 @@ gradle.buildFinished { def json = JsonOutput.toJson(output) outputFile.write json }`; - const gradleInitFile = join(localDir, 'renovate-plugin.gradle'); + const gradleInitFile = join(gradleRoot, 'renovate-plugin.gradle'); logger.debug( 'Creating renovate-plugin.gradle file with renovate gradle plugin' ); - await writeFile(gradleInitFile, content); + await writeLocalFile(gradleInitFile, content); } async function readGradleReport(localDir: string): Promise { @@ -59,11 +63,11 @@ async function readGradleReport(localDir: string): Promise { localDir, GRADLE_DEPENDENCY_REPORT_FILENAME ); - if (!(await exists(renovateReportFilename))) { + if (!(await localPathExists(renovateReportFilename))) { return []; } - const contents = await readFile(renovateReportFilename, 'utf8'); + const contents = await readLocalFile(renovateReportFilename, 'utf8'); try { return JSON.parse(contents); } catch (err) { diff --git a/lib/manager/gradle/deep/index.spec.ts b/lib/manager/gradle/deep/index.spec.ts index 14d6defd9b01a0..36f4ee06d5b11e 100644 --- a/lib/manager/gradle/deep/index.spec.ts +++ b/lib/manager/gradle/deep/index.spec.ts @@ -1,29 +1,32 @@ -import { exec as _exec } from 'child_process'; import type { Stats } from 'fs'; import os from 'os'; -import _fs from 'fs-extra'; import { join } from 'upath'; import { extractAllPackageFiles, updateDependency } from '..'; -import { envMock, mockExecAll } from '../../../../test/exec-util'; +import { envMock, exec, mockExecAll } from '../../../../test/exec-util'; import { addReplacingSerializer, + env, + fs, loadFixture, - mocked, } from '../../../../test/util'; import { setGlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; +import { + ReleaseResult, + getPkgReleases as _getPkgReleases, +} from '../../../datasource'; import * as docker from '../../../util/exec/docker'; -import * as _env from '../../../util/exec/env'; import type { ExtractConfig } from '../../types'; jest.mock('child_process'); -const exec: jest.Mock = _exec as never; - -jest.mock('fs-extra'); -const fs = mocked(_fs); - jest.mock('../../../util/exec/env'); -const env = mocked(_env); +jest.mock('../../../util/fs'); +jest.mock('../../../datasource'); + +const getPkgReleases: jest.MockInstance< + ReturnType, + jest.ArgsType +> = _getPkgReleases as never; const adminConfig: RepoGlobalConfig = { localDir: join('/foo/bar'), @@ -52,22 +55,34 @@ dependency "bar:bar:This.Is.Valid.Version.Good.Luck" dependency "baz:baz:\${bazVersion}" `; +const graddleWrapperPropertiesData = loadFixture( + '/gradle-wrappers/6/gradle/wrapper/gradle-wrapper.properties' +); + addReplacingSerializer('gradlew.bat', ''); addReplacingSerializer('./gradlew', ''); +const javaReleases: ReleaseResult = { + releases: [ + { version: '8.0.302' }, + { version: '11.0.12' }, + { version: '16.0.2' }, + ], +}; + describe('manager/gradle/deep/index', () => { const updatesReport = loadFixture('updatesReport.json'); function setupMocks({ - baseDir = '/foo/bar', wrapperFilename = `gradlew`, + wrapperPropertiesFilename = 'gradle/wrapper/gradle-wrapper.properties', pluginFilename = 'renovate-plugin.gradle', report = updatesReport, reportFilename = 'gradle-renovate-report.json', packageFilename = 'build.gradle', output = gradleOutput, } = {}) { - fs.stat.mockImplementationOnce((dirname) => { + fs.stat.mockImplementationOnce((_dirname) => { if (wrapperFilename) { return Promise.resolve({ isFile: () => true, @@ -75,14 +90,34 @@ describe('manager/gradle/deep/index', () => { } return Promise.reject(); }); - fs.writeFile.mockImplementationOnce((_filename, _content) => {}); - fs.exists.mockImplementationOnce((_filename) => Promise.resolve(!!report)); - fs.readFile.mockImplementationOnce((filename) => - report ? Promise.resolve(report as never) : Promise.reject() - ); - fs.readFile.mockImplementationOnce((filename) => - Promise.resolve(buildGradle as never) - ); + fs.writeLocalFile.mockImplementation((f, _content) => { + if (f?.endsWith(pluginFilename)) { + return Promise.resolve(); + } + return Promise.reject(); + }); + fs.localPathExists.mockImplementation((f) => { + if (f?.endsWith(reportFilename)) { + return Promise.resolve(!!report); + } + if (f?.endsWith(wrapperPropertiesFilename)) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + fs.readLocalFile.mockImplementation((f) => { + if (f?.endsWith(reportFilename)) { + return report ? Promise.resolve(report) : Promise.reject(); + } + if (f?.endsWith(packageFilename)) { + return Promise.resolve(buildGradle); + } + if (f?.endsWith(wrapperPropertiesFilename)) { + return Promise.resolve(graddleWrapperPropertiesData); + } + return Promise.resolve(''); + }); + return mockExecAll(exec, output); } @@ -107,18 +142,30 @@ describe('manager/gradle/deep/index', () => { const dependencies = await extractAllPackageFiles(config, [ 'build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle', + }, + ]); expect(execSnapshots).toMatchSnapshot(); }); it('should return gradle.kts dependencies', async () => { - const execSnapshots = setupMocks(); + const execSnapshots = setupMocks({ packageFilename: 'build.gradle.kts' }); const dependencies = await extractAllPackageFiles(config, [ 'build.gradle.kts', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle.kts', + }, + ]); expect(execSnapshots).toMatchSnapshot(); }); @@ -158,8 +205,14 @@ describe('manager/gradle/deep/index', () => { const dependencies = await extractAllPackageFiles(config, [ 'build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(3); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle', + }, + ]); expect(execSnapshots).toMatchSnapshot(); }); @@ -168,8 +221,14 @@ describe('manager/gradle/deep/index', () => { const dependencies = await extractAllPackageFiles(config, [ 'build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle', + }, + ]); expect(execSnapshots).toMatchSnapshot(); }); @@ -179,23 +238,42 @@ describe('manager/gradle/deep/index', () => { const dependencies = await extractAllPackageFiles(config, [ 'build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle', + }, + ]); expect(execSnapshots).toMatchSnapshot(); }); it('should execute gradle if gradlew is not available', async () => { - const execSnapshots = setupMocks({ wrapperFilename: null }); + const execSnapshots = setupMocks({ + wrapperFilename: null, + wrapperPropertiesFilename: null, + }); const dependencies = await extractAllPackageFiles(config, [ 'build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle', + }, + ]); expect(execSnapshots).toMatchSnapshot(); }); it('should return null and gradle should not be executed if no root build.gradle', async () => { - const execSnapshots = setupMocks({ wrapperFilename: null, report: null }); + const execSnapshots = setupMocks({ + wrapperFilename: null, + report: null, + wrapperPropertiesFilename: null, + }); const packageFiles = ['foo/build.gradle']; expect(await extractAllPackageFiles(config, packageFiles)).toBeNull(); expect(execSnapshots).toBeEmpty(); @@ -203,8 +281,9 @@ describe('manager/gradle/deep/index', () => { it('should return gradle dependencies for build.gradle in subdirectories if there is gradlew in the same directory', async () => { const execSnapshots = setupMocks({ - baseDir: '/foo/bar/', wrapperFilename: 'baz/qux/gradlew', + wrapperPropertiesFilename: + 'baz/qux/gradle/wrapper/gradle-wrapper.properties', packageFilename: 'baz/qux/build.gradle', reportFilename: 'baz/qux/gradle-renovate-report.json', pluginFilename: 'baz/qux/renovate-plugin.gradle', @@ -213,30 +292,55 @@ describe('manager/gradle/deep/index', () => { const dependencies = await extractAllPackageFiles(config, [ 'baz/qux/build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'baz/qux/build.gradle', + }, + ]); expect(execSnapshots).toMatchSnapshot(); }); it('should use docker if required', async () => { setGlobalConfig(dockerAdminConfig); - const execSnapshots = setupMocks({ wrapperFilename: null }); + const execSnapshots = setupMocks({ + wrapperFilename: null, + wrapperPropertiesFilename: null, + }); + getPkgReleases.mockResolvedValueOnce(javaReleases); const dependencies = await extractAllPackageFiles(config, [ 'build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle', + }, + ]); + expect(execSnapshots[0].cmd).toEqual('docker pull renovate/java:11.0.12'); expect(execSnapshots).toMatchSnapshot(); }); it('should use docker even if gradlew is available', async () => { setGlobalConfig(dockerAdminConfig); const execSnapshots = setupMocks(); + getPkgReleases.mockResolvedValueOnce(javaReleases); const dependencies = await extractAllPackageFiles(config, [ 'build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle', + }, + ]); + expect(execSnapshots[0].cmd).toEqual('docker pull renovate/java:11.0.12'); expect(execSnapshots).toMatchSnapshot(); }); @@ -244,11 +348,19 @@ describe('manager/gradle/deep/index', () => { setGlobalConfig(dockerAdminConfig); jest.spyOn(os, 'platform').mockReturnValueOnce('win32'); const execSnapshots = setupMocks({ wrapperFilename: 'gradlew.bat' }); + getPkgReleases.mockResolvedValueOnce(javaReleases); const dependencies = await extractAllPackageFiles(config, [ 'build.gradle', ]); - // FIXME: explicit assert condition - expect(dependencies).toMatchSnapshot(); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]?.deps).toHaveLength(8); + expect(dependencies).toMatchSnapshot([ + { + datasource: 'maven', + packageFile: 'build.gradle', + }, + ]); + expect(execSnapshots[0].cmd).toEqual('docker pull renovate/java:11.0.12'); expect(execSnapshots).toMatchSnapshot(); }); }); diff --git a/lib/manager/gradle/deep/index.ts b/lib/manager/gradle/deep/index.ts index 66c7135652baf7..bab24401d3ec65 100644 --- a/lib/manager/gradle/deep/index.ts +++ b/lib/manager/gradle/deep/index.ts @@ -1,5 +1,4 @@ import type { Stats } from 'fs'; -import { stat } from 'fs-extra'; import upath from 'upath'; import { getGlobalConfig } from '../../../config/global'; import { TEMPORARY_ERROR } from '../../../constants/error-messages'; @@ -7,7 +6,13 @@ import * as datasourceMaven from '../../../datasource/maven'; import { logger } from '../../../logger'; import { ExternalHostError } from '../../../types/errors/external-host-error'; import { ExecOptions, exec } from '../../../util/exec'; -import { readLocalFile } from '../../../util/fs'; +import { readLocalFile, stat } from '../../../util/fs'; +import { + extraEnv, + getJavaVersioning, + gradleWrapperFileName, + prepareGradleCommand, +} from '../../gradle-wrapper/utils'; import type { ExtractConfig, PackageFile, @@ -24,7 +29,7 @@ import { extractDependenciesFromUpdatesReport, } from './gradle-updates-report'; import type { GradleDependency } from './types'; -import { extraEnv, gradleWrapperFileName, prepareGradleCommand } from './utils'; +import { getDockerConstraint, getDockerPreCommands } from './utils'; export const GRADLE_DEPENDENCY_REPORT_OPTIONS = '--init-script renovate-plugin.gradle renovate'; @@ -46,16 +51,17 @@ async function prepareGradleCommandFallback( export async function executeGradle( config: ExtractConfig, cwd: string, - gradlew: Stats | null + gradlew: Stats | null, + gradleRoot = '.' ): Promise { let stdout: string; let stderr: string; - let timeout; + let timeout: number; if (config.gradle?.timeout) { timeout = config.gradle.timeout * 1000; } const cmd = await prepareGradleCommandFallback( - gradleWrapperFileName(config), + gradleWrapperFileName(), cwd, gradlew, GRADLE_DEPENDENCY_REPORT_OPTIONS @@ -64,7 +70,11 @@ export async function executeGradle( timeout, cwd, docker: { - image: 'gradle', + image: 'java', + tagConstraint: + config.constraints?.java ?? (await getDockerConstraint(gradleRoot)), + tagScheme: getJavaVersioning(), + preCommands: await getDockerPreCommands(gradleRoot), }, extraEnv, }; @@ -94,7 +104,7 @@ export async function extractAllPackageFiles( const { localDir } = getGlobalConfig(); for (const packageFile of packageFiles) { const dirname = upath.dirname(packageFile); - const gradlewPath = upath.join(dirname, gradleWrapperFileName(config)); + const gradlewPath = upath.join(dirname, gradleWrapperFileName()); gradlew = await stat(upath.join(localDir, gradlewPath)).catch(() => null); if (['build.gradle', 'build.gradle.kts'].includes(packageFile)) { @@ -114,14 +124,15 @@ export async function extractAllPackageFiles( } logger.debug('Extracting dependencies from all gradle files'); - const cwd = upath.join(localDir, upath.dirname(rootBuildGradle)); + const gradleRoot = upath.dirname(rootBuildGradle); + const cwd = upath.join(localDir, gradleRoot); - await createRenovateGradlePlugin(cwd); - await executeGradle(config, cwd, gradlew); + await createRenovateGradlePlugin(gradleRoot); + await executeGradle(config, cwd, gradlew, gradleRoot); init(); - const dependencies = await extractDependenciesFromUpdatesReport(cwd); + const dependencies = await extractDependenciesFromUpdatesReport(gradleRoot); if (dependencies.length === 0) { return []; } diff --git a/lib/manager/gradle/deep/utils.ts b/lib/manager/gradle/deep/utils.ts index 48afb4d804f3a5..4b8aa9e1181eec 100644 --- a/lib/manager/gradle/deep/utils.ts +++ b/lib/manager/gradle/deep/utils.ts @@ -1,44 +1,42 @@ -import type { Stats } from 'fs'; -import os from 'os'; -import { chmod } from 'fs-extra'; -import upath from 'upath'; +import { join } from 'upath'; import { getGlobalConfig } from '../../../config/global'; -import type { ExtractConfig } from '../../types'; - -export const extraEnv = { - GRADLE_OPTS: - '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.caching=false', -}; - -export function gradleWrapperFileName(config: ExtractConfig): string { - if ( - os.platform() === 'win32' && - getGlobalConfig()?.binarySource !== 'docker' - ) { - return 'gradlew.bat'; +import { localPathExists, readLocalFile } from '../../../util/fs'; +import { + extractGradleVersion, + getJavaContraint, +} from '../../gradle-wrapper/utils'; + +const GradleWrapperProperties = 'gradle/wrapper/gradle-wrapper.properties'; + +export async function getDockerConstraint( + gradleRoot: string +): Promise { + if (getGlobalConfig()?.binarySource !== 'docker') { + // ignore + return null; } - return './gradlew'; + + const fileContent = await readLocalFile( + join(gradleRoot, GradleWrapperProperties), + 'utf8' + ); + + const version = extractGradleVersion(fileContent); + + return getJavaContraint(version); } -export async function prepareGradleCommand( - gradlewName: string, - cwd: string, - gradlew: Stats | null, - args: string | null -): Promise { - /* eslint-disable no-bitwise */ - // istanbul ignore if - if (gradlew?.isFile() === true) { - // if the file is not executable by others - if ((gradlew.mode & 0o1) === 0) { - // add the execution permission to the owner, group and others - await chmod(upath.join(cwd, gradlewName), gradlew.mode | 0o111); - } - if (args === null) { - return gradlewName; - } - return `${gradlewName} ${args}`; +export async function getDockerPreCommands( + gradleRoot: string +): Promise { + if (getGlobalConfig()?.binarySource !== 'docker') { + // ignore + return null; + } + + if (await localPathExists(join(gradleRoot, GradleWrapperProperties))) { + return null; } - /* eslint-enable no-bitwise */ - return null; + + return ['install-tool gradle latest']; } diff --git a/lib/manager/types.ts b/lib/manager/types.ts index caed04ef60d980..000c830f0db307 100644 --- a/lib/manager/types.ts +++ b/lib/manager/types.ts @@ -18,6 +18,7 @@ export interface ManagerData { } export interface ExtractConfig { + constraints?: Record; registryUrls?: string[]; endpoint?: string; gradle?: { timeout?: number };