Skip to content
Snippets Groups Projects
Unverified Commit b8976581 authored by Joshua Gleitze's avatar Joshua Gleitze Committed by GitHub
Browse files

feat(gradle): chmod gradlew (correctly) (#5823)

parent e5ff4c0f
Branches
Tags
No related merge requests found
......@@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: [
'lib/**/*.{js,ts}',
'!lib/**/*.{d,spec}.ts',
'!lib/**/{__fixtures__,__mocks__}/**/*.{js,ts}',
'!lib/**/{__fixtures__,__mocks__,__testutil__}/**/*.{js,ts}',
],
coverageReporters: ci
? ['html', 'json', 'text-summary']
......
......@@ -16,7 +16,29 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
`;
exports[`manager/gradle extractPackageFile should execute gradle if gradlew is not available 1`] = `
Array [
Object {
"cmd": "gradle --init-script renovate-plugin.gradle renovate",
"options": Object {
"cwd": "localDir",
"encoding": "utf-8",
"env": Object {
"HOME": "/home/user",
"HTTPS_PROXY": "https://example.com",
"HTTP_PROXY": "http://example.com",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US",
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 60000,
},
},
]
......@@ -38,7 +60,29 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
`;
exports[`manager/gradle extractPackageFile should execute gradlew.bat when available on Windows 1`] = `
Array [
Object {
"cmd": "gradlew.bat --init-script renovate-plugin.gradle renovate",
"options": Object {
"cwd": "localDir",
"encoding": "utf-8",
"env": Object {
"HOME": "/home/user",
"HTTPS_PROXY": "https://example.com",
"HTTP_PROXY": "http://example.com",
"LANG": "en_US.UTF-8",
"LC_ALL": "en_US",
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 60000,
},
},
]
......@@ -60,7 +104,7 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
......@@ -82,7 +126,7 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
......@@ -91,7 +135,7 @@ Array [
exports[`manager/gradle extractPackageFile should return empty if there is no dependency report 1`] = `
Array [
Object {
"cmd": "gradle --init-script renovate-plugin.gradle renovate",
"cmd": "./gradlew --init-script renovate-plugin.gradle renovate",
"options": Object {
"cwd": "localDir",
"encoding": "utf-8",
......@@ -104,7 +148,7 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
......@@ -247,7 +291,7 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
......@@ -332,7 +376,7 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
......@@ -475,7 +519,7 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
......@@ -483,10 +527,22 @@ Array [
exports[`manager/gradle extractPackageFile should return null and gradle should not be executed if no root build.gradle 1`] = `Array []`;
exports[`manager/gradle extractPackageFile should run gradlew through \`sh\` when available but not executable 1`] = `
exports[`manager/gradle extractPackageFile should use docker even if gradlew is available 1`] = `
Array [
Object {
"cmd": "sh gradlew --init-script renovate-plugin.gradle renovate",
"cmd": "docker pull renovate/gradle",
"options": Object {
"encoding": "utf-8",
},
},
Object {
"cmd": "docker ps --filter name=renovate_gradle -aq | xargs --no-run-if-empty docker rm -f",
"options": Object {
"encoding": "utf-8",
},
},
Object {
"cmd": "docker run --rm --name=renovate_gradle --label=renovate_child -v \\"localDir\\":\\"localDir\\" -w \\"localDir\\" renovate/gradle bash -l -c \\"./gradlew --init-script renovate-plugin.gradle renovate\\"",
"options": Object {
"cwd": "localDir",
"encoding": "utf-8",
......@@ -499,14 +555,20 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 900000,
},
},
]
`;
exports[`manager/gradle extractPackageFile should use docker even if gradlew is available 1`] = `
exports[`manager/gradle extractPackageFile should use docker even if gradlew.bat is available on Windows 1`] = `
Array [
Object {
"cmd": "docker pull renovate/gradle",
"options": Object {
"encoding": "utf-8",
},
},
Object {
"cmd": "docker ps --filter name=renovate_gradle -aq | xargs --no-run-if-empty docker rm -f",
"options": Object {
......@@ -561,7 +623,7 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
......@@ -623,7 +685,7 @@ Array [
"NO_PROXY": "localhost",
"PATH": "/tmp/path",
},
"timeout": 20000,
"timeout": 60000,
},
},
]
......
import { spawnSync, SpawnSyncReturns } from 'child_process';
const failIfNoJavaEnv = 'CI';
const gradleJavaVersionSupport = {
5: { min: 8, max: 12 },
6: { min: 8, max: 13 },
};
const skipJava = process.env.SKIP_JAVA_TESTS === 'true';
const enforceJava = process.env[failIfNoJavaEnv] === 'true' && !skipJava;
function parseJavaVersion(javaVersionOutput: string): number {
const versionMatch = /version "(?:1\.)?(\d+)[\d._-]*"/.exec(
javaVersionOutput
);
if (versionMatch !== null && versionMatch.length === 2) {
return parseInt(versionMatch[1], 10);
}
if (enforceJava) {
throw Error(`This test suite needs Java and ${failIfNoJavaEnv} is set. However, we cannot parse the Java version.
The output of java -version was:
${javaVersionOutput}`);
}
return 0;
}
let cachedJavaVersion: number | null = null;
function determineJavaVersion(): number {
if (cachedJavaVersion == null) {
let javaVersionCommand: SpawnSyncReturns<string>;
let error: Error;
try {
javaVersionCommand = spawnSync('java', ['-version'], {
encoding: 'utf8',
windowsHide: true,
});
} catch (e) {
error = e;
}
if (javaVersionCommand.error) {
error = javaVersionCommand.error;
}
if (error) {
if (!enforceJava) {
return 0;
}
throw Error(
`This test suite needs Java and ${failIfNoJavaEnv} is set.
Result of java -version:
${error}`
);
}
cachedJavaVersion = parseJavaVersion(javaVersionCommand.stderr);
}
return cachedJavaVersion;
}
class WithGradle {
private gradleSupportsThisJavaVersion: boolean;
constructor(private gradleVersion: number) {
const javaVersion = determineJavaVersion();
if (gradleJavaVersionSupport[gradleVersion] === undefined) {
throw Error(`Unknown gradle version '${gradleVersion}'!`);
}
const supportedJavaVersions = gradleJavaVersionSupport[gradleVersion];
this.gradleSupportsThisJavaVersion =
javaVersion >= supportedJavaVersions.min &&
javaVersion <= supportedJavaVersions.max;
if (!this.gradleSupportsThisJavaVersion && enforceJava) {
throw Error(
`This test needs a Java version between ${supportedJavaVersions.min} and ${supportedJavaVersions.max}. The current Java version is ${javaVersion} and ${failIfNoJavaEnv} is set!`
);
}
}
get it(): jest.It {
return !this.gradleSupportsThisJavaVersion || skipJava ? it.skip : it;
}
}
export function ifSystemSupportsGradle(gradleVersion: number): WithGradle {
return new WithGradle(gradleVersion);
}
import tmp, { DirectoryResult } from 'tmp-promise';
import * as fs from 'fs-extra';
import * as path from 'path';
import { spawnSync, SpawnSyncReturns } from 'child_process';
import { exec } from '../../util/exec';
import { GRADLE_DEPENDENCY_REPORT_OPTIONS } from './index';
import {
createRenovateGradlePlugin,
GRADLE_DEPENDENCY_REPORT_FILENAME,
} from './gradle-updates-report';
import { ifSystemSupportsGradle } from './__testutil__/gradle';
const fixtures = 'lib/manager/gradle/__fixtures__';
const failIfNoJavaEnv = 'CI';
const gradleJavaVersionSupport = {
5: { min: 8, max: 12 },
6: { min: 8, max: 13 },
};
const skipJava = process.env.SKIP_JAVA_TESTS === 'true';
const enforceJava = process.env[failIfNoJavaEnv] === 'true' && !skipJava;
function parseJavaVersion(javaVersionOutput: string) {
const versionMatch = /version "(?:1\.)?(\d+)[\d._-]*"/.exec(
javaVersionOutput
);
if (versionMatch !== null && versionMatch.length === 2) {
return parseInt(versionMatch[1], 10);
}
if (enforceJava) {
throw Error(`This test suite needs Java and ${failIfNoJavaEnv} is set. However, we cannot parse the Java version.
The output of java -version was:
${javaVersionOutput}`);
}
return 0;
}
function determineJavaVersion(): number {
let javaVersionCommand: SpawnSyncReturns<string>;
let error: Error;
try {
javaVersionCommand = spawnSync('java', ['-version'], {
encoding: 'utf8',
windowsHide: true,
});
} catch (e) {
error = e;
}
if (javaVersionCommand.error) {
error = javaVersionCommand.error;
}
if (error) {
if (!enforceJava) {
return 0;
}
throw Error(
`This test suite needs Java and ${failIfNoJavaEnv} is set.
Result of java -version:
${error}`
);
}
return parseJavaVersion(javaVersionCommand.stderr);
}
describe('lib/manager/gradle/gradle-updates-report', () => {
let workingDir: DirectoryResult;
const javaVersion = determineJavaVersion();
beforeEach(async () => {
workingDir = await tmp.dir({ unsafeCleanup: true });
......@@ -72,16 +20,7 @@ describe('lib/manager/gradle/gradle-updates-report', () => {
describe('createRenovateGradlePlugin', () => {
for (const gradleVersion of [5, 6]) {
const supportedJavaVersions = gradleJavaVersionSupport[gradleVersion];
const gradleSupportsThisJavaVersion =
javaVersion >= supportedJavaVersions.min &&
javaVersion <= supportedJavaVersions.max;
if (!gradleSupportsThisJavaVersion && enforceJava) {
throw Error(
`This test needs a Java version between ${supportedJavaVersions.min} and ${supportedJavaVersions.max}. The current Java version is ${javaVersion} and ${failIfNoJavaEnv} is set!`
);
}
(!gradleSupportsThisJavaVersion || skipJava ? it.skip : it)(
ifSystemSupportsGradle(gradleVersion).it(
`generates a report for Gradle version ${gradleVersion}`,
// the function creation is correct and intended
// eslint-disable-next-line no-loop-func
......
import { toUnix } from 'upath';
import _fs from 'fs-extra';
import fsReal from 'fs';
import fsReal, { Stats } from 'fs';
import { exec as _exec } from 'child_process';
import * as manager from '.';
import { platform as _platform, Platform } from '../../platform';
import * as _os from 'os';
import tmp, { DirectoryResult } from 'tmp-promise';
import * as path from 'path';
import { platform as _platform } from '../../platform';
import { envMock, mockExecAll } from '../../../test/execUtil';
import * as _env from '../../util/exec/env';
import { mocked } from '../../../test/util';
import { BinarySource } from '../../util/exec/common';
import { setUtilConfig } from '../../util';
import * as _docker from '../../util/exec/docker';
import * as _util from '../../util';
import { ifSystemSupportsGradle } from './__testutil__/gradle';
import * as _manager from '.';
import { ExtractConfig } from '../common';
jest.mock('fs-extra');
jest.mock('child_process');
jest.mock('../../util/exec/env');
const platform: jest.Mocked<Platform> = _platform as any;
const fs: jest.Mocked<typeof _fs> = _fs as any;
const exec: jest.Mock<typeof _exec> = _exec as any;
const env = mocked(_env);
const fixtures = 'lib/manager/gradle/__fixtures__';
const config = {
localDir: 'localDir',
gradle: {
timeout: 20,
timeout: 60,
},
};
......@@ -36,22 +34,64 @@ const gradleOutput = {
stderr: '',
};
describe('manager/gradle', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.resetModules();
fs.readFile.mockResolvedValue(updatesDependenciesReport as any);
fs.mkdir.mockResolvedValue();
fs.exists.mockResolvedValue(true);
fs.access.mockResolvedValue(undefined);
platform.getFile.mockResolvedValue('some content');
env.getChildProcessEnv.mockReturnValue(envMock.basic);
setUtilConfig(config);
});
function resetMocks() {
jest.resetAllMocks();
jest.resetModules();
}
async function setupMocks() {
resetMocks();
jest.mock('fs-extra');
jest.mock('child_process');
jest.mock('../../util/exec/env');
jest.mock('../../platform');
jest.mock('os');
const fs: jest.Mocked<typeof _fs> = require('fs-extra');
const os: jest.Mocked<typeof _os> = require('os');
const platform: jest.Mocked<typeof _platform> = require('../../platform')
.platform;
const env: jest.Mocked<typeof _env> = require('../../util/exec/env');
const exec: jest.Mock<typeof _exec> = require('child_process').exec;
const util: jest.Mocked<typeof _util> = require('../../util');
platform.getFile.mockResolvedValue('some content');
env.getChildProcessEnv.mockReturnValue(envMock.basic);
await util.setUtilConfig(config);
return [require('.'), exec, fs, util, os];
}
describe('manager/gradle', () => {
describe('extractPackageFile', () => {
let manager: typeof _manager;
let exec: jest.Mock<typeof _exec>;
let fs: jest.Mocked<typeof _fs>;
let util: jest.Mocked<typeof _util>;
let os: jest.Mocked<typeof _os>;
let docker: typeof _docker;
beforeAll(async () => {
[manager, exec, fs, util, os] = await setupMocks();
docker = require('../../util/exec/docker');
});
afterAll(resetMocks);
beforeEach(() => {
fs.readFile.mockResolvedValue(updatesDependenciesReport as any);
fs.mkdir.mockResolvedValue();
fs.exists.mockResolvedValue(true);
fs.stat.mockResolvedValue({
mode: 0o755,
isFile: () => true,
} as Stats);
exec.mockReset();
docker.resetPrefetchedImages();
os.platform.mockReturnValue('linux');
});
it('should return gradle dependencies', async () => {
const execSnapshots = mockExecAll(exec, gradleOutput);
......@@ -141,18 +181,25 @@ describe('manager/gradle', () => {
expect(execSnapshots).toMatchSnapshot();
});
it('should run gradlew through `sh` when available but not executable', async () => {
it('should execute gradlew.bat when available on Windows', async () => {
const execSnapshots = mockExecAll(exec, gradleOutput);
os.platform.mockReturnValue('win32');
fs.access.mockRejectedValue(undefined);
await manager.extractAllPackageFiles(config, ['build.gradle']);
expect(execSnapshots).toMatchSnapshot();
});
it('should return null and gradle should not be executed if no root build.gradle', async () => {
it('should execute gradle if gradlew is not available', async () => {
const execSnapshots = mockExecAll(exec, gradleOutput);
fs.stat.mockRejectedValue(new Error());
fs.exists.mockResolvedValue(false);
await manager.extractAllPackageFiles(config, ['build.gradle']);
expect(execSnapshots).toMatchSnapshot();
});
it('should return null and gradle should not be executed if no root build.gradle', async () => {
const execSnapshots = mockExecAll(exec, gradleOutput);
fs.stat.mockRejectedValue(new Error());
const packageFiles = ['foo/build.gradle'];
expect(
......@@ -185,7 +232,7 @@ describe('manager/gradle', () => {
});
it('should use docker if required', async () => {
setUtilConfig({ ...config, binarySource: BinarySource.Docker });
util.setUtilConfig({ ...config, binarySource: BinarySource.Docker });
const execSnapshots = mockExecAll(exec, gradleOutput);
const configWithDocker = {
......@@ -198,7 +245,21 @@ describe('manager/gradle', () => {
});
it('should use docker even if gradlew is available', async () => {
setUtilConfig({ ...config, binarySource: BinarySource.Docker });
util.setUtilConfig({ ...config, binarySource: BinarySource.Docker });
const execSnapshots = mockExecAll(exec, gradleOutput);
const configWithDocker = {
binarySource: BinarySource.Docker,
...config,
gradle: {},
};
await manager.extractAllPackageFiles(configWithDocker, ['build.gradle']);
expect(execSnapshots).toMatchSnapshot();
});
it('should use docker even if gradlew.bat is available on Windows', async () => {
os.platform.mockReturnValue('win32');
const execSnapshots = mockExecAll(exec, gradleOutput);
const configWithDocker = {
......@@ -213,6 +274,14 @@ describe('manager/gradle', () => {
});
describe('updateDependency', () => {
let manager: typeof _manager;
let exec: jest.Mock<typeof _exec>;
beforeAll(async () => {
[manager, exec] = await setupMocks();
});
afterAll(resetMocks);
it('should update an existing module dependency', () => {
const execSnapshots = mockExecAll(exec, gradleOutput);
......@@ -303,4 +372,70 @@ describe('manager/gradle', () => {
expect(execSnapshots).toMatchSnapshot();
});
});
describe('executeGradle integration', () => {
const SUCCESS_FILE = 'success.indicator';
let workingDir: DirectoryResult;
let testRunConfig: ExtractConfig;
const manager = require('.');
beforeEach(async () => {
workingDir = await tmp.dir({ unsafeCleanup: true });
testRunConfig = { ...config, localDir: workingDir.path };
await _fs.copy(`${fixtures}/minimal-project`, workingDir.path);
await _fs.copy(`${fixtures}/gradle-wrappers/6`, workingDir.path);
const mockPluginContent = `
allprojects {
tasks.register("renovate") {
doLast {
new File('${SUCCESS_FILE}').write 'success'
}
}
}`;
await _fs.writeFile(
path.join(workingDir.path, 'renovate-plugin.gradle'),
mockPluginContent
);
});
ifSystemSupportsGradle(6).it(
'executes an executable gradle wrapper',
async () => {
const gradlew = await fsReal.promises.stat(
path.join(workingDir.path, 'gradlew')
);
await manager.executeGradle(testRunConfig, workingDir.path, gradlew);
await expect(
fsReal.promises.readFile(
path.join(workingDir.path, SUCCESS_FILE),
'utf8'
)
).resolves.toBe('success');
},
120000
);
ifSystemSupportsGradle(6).it(
'executes a not-executable gradle wrapper',
async () => {
await fsReal.promises.chmod(
path.join(workingDir.path, 'gradlew'),
'444'
);
const gradlew = await fsReal.promises.stat(
path.join(workingDir.path, 'gradlew')
);
await manager.executeGradle(testRunConfig, workingDir.path, gradlew);
await expect(
fsReal.promises.readFile(
path.join(workingDir.path, SUCCESS_FILE),
'utf8'
)
).resolves.toBe('success');
},
120000
);
});
});
import { access, constants, exists } from 'fs-extra';
import * as os from 'os';
import * as fs from 'fs-extra';
import { Stats } from 'fs';
import upath from 'upath';
import { exec, ExecOptions } from '../../util/exec';
import { logger } from '../../logger';
import * as mavenVersioning from '../../versioning/maven';
import {
init,
collectVersionVariables,
updateGradleVersion,
GradleDependency,
} from './build-gradle';
import {
createRenovateGradlePlugin,
extractDependenciesFromUpdatesReport,
} from './gradle-updates-report';
import {
PackageFile,
ExtractConfig,
Upgrade,
PackageFile,
UpdateDependencyConfig,
Upgrade,
} from '../common';
import { platform } from '../../platform';
import { LANGUAGE_JAVA } from '../../constants/languages';
import * as datasourceMaven from '../../datasource/maven';
import { DatasourceError } from '../../datasource';
import { BinarySource } from '../../util/exec/common';
import {
collectVersionVariables,
GradleDependency,
init,
updateGradleVersion,
} from './build-gradle';
import {
createRenovateGradlePlugin,
extractDependenciesFromUpdatesReport,
} from './gradle-updates-report';
export const GRADLE_DEPENDENCY_REPORT_OPTIONS =
'--init-script renovate-plugin.gradle renovate';
const TIMEOUT_CODE = 143;
async function canExecute(path: string): Promise<boolean> {
try {
await access(path, constants.X_OK);
return true;
} catch {
return false;
function gradleWrapperFileName(config: ExtractConfig): string {
if (
os.platform() === 'win32' &&
config.binarySource !== BinarySource.Docker
) {
return 'gradlew.bat';
}
return './gradlew';
}
async function getGradleCommandLine(
async function prepareGradleCommandLine(
config: ExtractConfig,
cwd: string
cwd: string,
gradlew: Stats | null
): Promise<string> {
const args = GRADLE_DEPENDENCY_REPORT_OPTIONS;
const gradlewName = gradleWrapperFileName(config);
if (gradlew?.isFile() === true) {
// if the file is not executable by others
// eslint-disable-next-line no-bitwise
if ((gradlew.mode & 0o1) === 0) {
// add the execution permission to the owner, group and others
// eslint-disable-next-line no-bitwise
await fs.chmod(upath.join(cwd, gradlewName), gradlew.mode | 0o111);
}
const gradlewPath = upath.join(cwd, 'gradlew');
const gradlewExists = await exists(gradlewPath);
const gradlewExecutable = gradlewExists && (await canExecute(gradlewPath));
if (gradlewExecutable) {
return `./gradlew ${args}`;
}
if (gradlewExists) {
return `sh gradlew ${args}`;
return `${gradlewName} ${args}`;
}
return `gradle ${args}`;
}
async function executeGradle(
export async function executeGradle(
config: ExtractConfig,
cwd: string
cwd: string,
gradlew: Stats | null
): Promise<void> {
let stdout: string;
let stderr: string;
......@@ -68,7 +75,7 @@ async function executeGradle(
config.gradle && config.gradle.timeout
? config.gradle.timeout * 1000
: undefined;
const cmd = await getGradleCommandLine(config, cwd);
const cmd = await prepareGradleCommandLine(config, cwd, gradlew);
const execOptions: ExecOptions = {
timeout,
cwd,
......@@ -97,19 +104,21 @@ export async function extractAllPackageFiles(
packageFiles: string[]
): Promise<PackageFile[] | null> {
let rootBuildGradle: string | undefined;
let gradlew: Stats | null;
for (const packageFile of packageFiles) {
const dirname = upath.dirname(packageFile);
const gradlewPath = upath.join(dirname, gradleWrapperFileName(config));
gradlew = await fs
.stat(upath.join(config.localDir, gradlewPath))
.catch(() => null);
if (['build.gradle', 'build.gradle.kts'].includes(packageFile)) {
rootBuildGradle = packageFile;
break;
}
// If there is gradlew in the same directory, the directory should be a Gradle project root
const dirname = upath.dirname(packageFile);
const gradlewPath = upath.join(dirname, 'gradlew');
const gradlewExists = await exists(
upath.join(config.localDir, gradlewPath)
);
if (gradlewExists) {
if (gradlew?.isFile() === true) {
rootBuildGradle = packageFile;
break;
}
......@@ -123,7 +132,7 @@ export async function extractAllPackageFiles(
const cwd = upath.join(config.localDir, upath.dirname(rootBuildGradle));
await createRenovateGradlePlugin(cwd);
await executeGradle(config, cwd);
await executeGradle(config, cwd, gradlew);
init();
......
......@@ -14,6 +14,7 @@
"./dist",
"**/__mocks__/**",
"**/__fixtures__/**",
"**/__testutil__/**",
"**/*.spec.ts",
"./test"
]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment