Skip to content

Commit

Permalink
feat: install JDK automatically if missing (react-native-community#1065)
Browse files Browse the repository at this point in the history
* feat: install JDK automatically if missing on Windows

Install JDK@11 on Windows if missing. Later version break Gradle and
prior versions do not provide a .zip for easy local installation.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Fix react-native-community#980

* chore: pr feedback

* chore: replace node-unzipper with node-stream-zip

* chore: pr feedback

* chore: pr feedback
  • Loading branch information
molant authored Apr 6, 2020
1 parent e87b6b5 commit 6c72d03
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 54 deletions.
66 changes: 66 additions & 0 deletions packages/cli-types/src/node-stream-zip.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// https://github.com/antelle/node-stream-zip/issues/36#issuecomment-596249657
declare module 'node-stream-zip' {
import {Stream} from 'stream';

interface StreamZipOptions {
/**
* File to read
*/
file: string;
/**
* You will be able to work with entries inside zip archive, otherwise the only way to access them is entry event
*
* default: true
*/
storeEntries?: boolean;
/**
* By default, entry name is checked for malicious characters, like ../ or c:\123, pass this flag to disable validation errors
*
* default: false
*/
skipEntryNameValidation?: boolean;
/**
* Undocumented adjustment of chunk size
*
* default: automatic
*/
chunkSize?: number;
}

class ZipEntry {
name: string;
isDirectory: boolean;
isFile: boolean;
comment: string;
size: number;
}

class StreamZip {
constructor(config: StreamZipOptions);

on(event: 'ready', handler: () => void): void;

entry(entry: string): ZipEntry;
entries(): ZipEntry[];

entriesCount: number;

stream(
entry: string,
callback: (err: any | null, stream?: Stream) => void,
): void;
entryDataSync(entry: string): Buffer;
openEntry(
entry: string,
callback: (err: any | null, entry?: ZipEntry) => void,
sync: boolean,
): void;
extract(
entry: string | null,
outPath: string,
callback: (err?: any) => void,
): void;
close(callback?: (err?: any) => void): void;
}
export = StreamZip;
}
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"metro-react-native-babel-transformer": "^0.58.0",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"node-stream-zip": "^1.9.1",
"open": "^6.2.0",
"ora": "^3.4.0",
"pretty-format": "^25.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import execa from 'execa';
import jdk from '../jdk';
import getEnvironmentInfo from '../../../../tools/envinfo';
import {EnvironmentInfo} from '../../types';
import {NoopLoader} from '../../../../tools/loader';
import * as common from '../common';
import * as unzip from '../../../../tools/unzip';
import * as deleteFile from '../../../../tools/deleteFile';

jest.mock('execa', () => jest.fn());
jest
.spyOn(deleteFile, 'deleteFile')
.mockImplementation(() => Promise.resolve());

const mockFetchToTemp = jest.fn();
jest.mock('@react-native-community/cli-tools', () => {
return {
fetchToTemp: mockFetchToTemp,
};
});

const logSpy = jest.spyOn(common, 'logManualInstallation');

describe('jdk', () => {
let environmentInfo: EnvironmentInfo;

beforeAll(async () => {
environmentInfo = await getEnvironmentInfo();
}, 15000);

afterEach(() => {
jest.resetAllMocks();
});

it('returns a message if JDK is not installed', async () => {
environmentInfo.Languages.Java = 'Not Found';
((execa as unknown) as jest.Mock).mockResolvedValue({stdout: ''});
const diagnostics = await jdk.getDiagnostics(environmentInfo);
expect(diagnostics.needsToBeFixed).toBe(true);
});

it('returns false if JDK version is in range', async () => {
// @ts-ignore
environmentInfo.Languages.Java = {
version: '9',
};

const diagnostics = await jdk.getDiagnostics(environmentInfo);
expect(diagnostics.needsToBeFixed).toBe(false);
});

it('returns true if JDK version is not in range', async () => {
// @ts-ignore
environmentInfo.Languages.Java = {
version: '7',
};

const diagnostics = await jdk.getDiagnostics(environmentInfo);
expect(diagnostics.needsToBeFixed).toBe(true);
});

it('logs manual installation steps to the screen for the default fix', async () => {
const loader = new NoopLoader();
await jdk.runAutomaticFix({loader, environmentInfo});
expect(logSpy).toHaveBeenCalledTimes(1);
});

it('downloads and unzips JDK on Windows when missing', async () => {
const loader = new NoopLoader();
const loaderSucceedSpy = jest.spyOn(loader, 'succeed');
const loaderFailSpy = jest.spyOn(loader, 'fail');
const unzipSpy = jest
.spyOn(unzip, 'unzip')
.mockImplementation(() => Promise.resolve());

await jdk.win32AutomaticFix({loader, environmentInfo});

expect(loaderFailSpy).toHaveBeenCalledTimes(0);
expect(logSpy).toHaveBeenCalledTimes(0);
expect(unzipSpy).toBeCalledTimes(1);
expect(loaderSucceedSpy).toBeCalledWith(
'JDK installed successfully. Please restart your shell to see the changes',
);
});
});
2 changes: 2 additions & 0 deletions packages/cli/src/commands/doctor/healthchecks/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import nodeJS from './nodeJS';
import {yarn, npm} from './packageManagers';
import jdk from './jdk';
import python from './python';
import watchman from './watchman';
import androidHomeEnvVariable from './androidHomeEnvVariable';
Expand Down Expand Up @@ -34,6 +35,7 @@ export const getHealthchecks = ({contributor}: Options): Healthchecks => ({
android: {
label: 'Android',
healthchecks: [
jdk,
androidHomeEnvVariable,
androidSDK,
...(contributor ? [androidNDK] : []),
Expand Down
65 changes: 65 additions & 0 deletions packages/cli/src/commands/doctor/healthchecks/jdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {join} from 'path';
import versionRanges from '../versionRanges';
import {doesSoftwareNeedToBeFixed} from '../checkInstallation';
import {logManualInstallation} from './common';
import {HealthCheckInterface} from '../types';

import {downloadAndUnzip} from '../../../tools/downloadAndUnzip';
import {
setEnvironment,
updateEnvironment,
} from '../../../tools/environmentVariables';
import {Ora} from 'ora';

export default {
label: 'JDK',
getDiagnostics: async ({Languages}) => ({
needsToBeFixed: doesSoftwareNeedToBeFixed({
version:
typeof Languages.Java === 'string'
? Languages.Java
: Languages.Java.version,
versionRange: versionRanges.JAVA,
}),

version:
typeof Languages.Java === 'string'
? Languages.Java
: Languages.Java.version,
versionRange: versionRanges.JAVA,
}),
win32AutomaticFix: async ({loader}: {loader: Ora}) => {
try {
// Installing JDK 11 because later versions seem to cause issues with gradle at the moment
const installerUrl =
'https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_windows-x64_bin.zip';
const installPath = process.env.LOCALAPPDATA || ''; // The zip is in a folder `jdk-11.02` so it can be unzipped directly there

await downloadAndUnzip({
loader,
downloadUrl: installerUrl,
component: 'JDK',
installPath,
});

loader.text = 'Updating environment variables';

const jdkPath = join(installPath, 'jdk-11.0.2');

await setEnvironment('JAVA_HOME', jdkPath);
await updateEnvironment('PATH', join(jdkPath, 'bin'));

loader.succeed(
'JDK installed successfully. Please restart your shell to see the changes',
);
} catch (e) {
loader.fail(e);
}
},
runAutomaticFix: async () => {
logManualInstallation({
healthcheck: 'JDK',
url: 'https://openjdk.java.net/',
});
},
} as HealthCheckInterface;
78 changes: 26 additions & 52 deletions packages/cli/src/commands/doctor/types.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,50 @@
import {Ora} from 'ora';

type NotFound = 'Not Found';
type AvailableInformation = {
version: string;
path: string;
};

type Information = AvailableInformation | NotFound;

export type EnvironmentInfo = {
System: {
OS: string;
CPU: string;
Memory: string;
Shell: {
version: string;
path: string;
};
Shell: AvailableInformation;
};
Binaries: {
Node: {
version: string;
path: string;
};
Yarn: {
version: string;
path: string;
};
npm: {
version: string;
path: string;
};
Watchman: {
version: string;
path: string;
};
Node: AvailableInformation;
Yarn: AvailableInformation;
npm: AvailableInformation;
Watchman: AvailableInformation;
};
SDKs: {
'iOS SDK': {
Platforms: string[];
};
'Android SDK':
| {
'API Levels': string[] | 'Not Found';
'Build Tools': string[] | 'Not Found';
'System Images': string[] | 'Not Found';
'Android NDK': string | 'Not Found';
'API Levels': string[] | NotFound;
'Build Tools': string[] | NotFound;
'System Images': string[] | NotFound;
'Android NDK': string | NotFound;
}
| 'Not Found';
| NotFound;
};
IDEs: {
'Android Studio': string;
Emacs: {
version: string;
path: string;
};
Nano: {
version: string;
path: string;
};
VSCode: {
version: string;
path: string;
};
Vim: {
version: string;
path: string;
};
Xcode: {
version: string;
path: string;
};
Emacs: AvailableInformation;
Nano: AvailableInformation;
VSCode: AvailableInformation;
Vim: AvailableInformation;
Xcode: AvailableInformation;
};
Languages: {
Python:
| {
version: string;
path: string;
}
| 'Not Found';
Java: Information;
Python: Information;
};
};

Expand Down Expand Up @@ -112,7 +86,7 @@ export type HealthCheckInterface = {
export type HealthCheckResult = {
label: string;
needsToBeFixed: boolean;
version?: 'Not Found' | string;
version?: NotFound | string;
versions?: [string] | string;
versionRange?: string;
description: string | undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/doctor/versionRanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default {
NPM: '>= 4.x',
WATCHMAN: '4.x',
PYTHON: '>= 2.x < 3',
JAVA: '>= 8',
// Android
ANDROID_SDK: '>= 26.x',
ANDROID_NDK: '>= 19.x',
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/tools/deleteFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {unlink} from 'fs';
import {promisify} from 'util';

export const deleteFile = promisify(unlink);
Loading

0 comments on commit 6c72d03

Please sign in to comment.