Skip to content

Commit ad1462f

Browse files
committed
Getting started improvements
When build or run command is executed, checks if the environment is properly configured for executing local builds. If it is not properly configured, {N} CLI shows a prompt and user is able to choice if to run setup script or use cloud builds. If cloud builds option is selected, {N} CLI installs the nativescript-cloud extension and prints that user can use the $ tns login command to log in with his/her account and then $ tns cloud run/build android/ios command to build his/her app in the cloud. If setup script option is selected, {N} CLI runs the setup script to try and automatically configure the environment. If the environment is not properly configured after executing the setup script, {N} CLI shows a prompt and user is able to choice if to use cloud builds or manually setup the environment. If the manually setup option is selected, {N} CLI prints: "To be able to build for android/ios, verify that your environment is configured according to the system requirements described at https://docs.nativescript.org/start/ns-setup-os-x.}"
1 parent 069e69f commit ad1462f

File tree

10 files changed

+373
-186
lines changed

10 files changed

+373
-186
lines changed

lib/android-tools-info.ts

Lines changed: 11 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import * as path from "path";
22
import * as semver from "semver";
3-
import { EOL } from "os";
43
import { cache } from "./common/decorators";
5-
import { appendZeroesToVersion } from './common/helpers';
4+
import { androidToolsInfo } from "nativescript-doctor";
65

76
export class AndroidToolsInfo implements IAndroidToolsInfo {
87
private static ANDROID_TARGET_PREFIX = "android";
98
private static SUPPORTED_TARGETS = ["android-17", "android-18", "android-19", "android-21", "android-22", "android-23", "android-24", "android-25", "android-26", "android-27"];
109
private static MIN_REQUIRED_COMPILE_TARGET = 22;
1110
private static REQUIRED_BUILD_TOOLS_RANGE_PREFIX = ">=23";
1211
private static VERSION_REGEX = /((\d+\.){2}\d+)/;
13-
private static MIN_JAVA_VERSION = "1.8.0";
14-
private static MAX_JAVA_VERSION = "1.9.0";
1512

1613
private showWarningsAsErrors: boolean;
1714
private toolsInfo: IAndroidToolsInfoData;
@@ -20,10 +17,8 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
2017
return process.env["ANDROID_HOME"];
2118
}
2219

23-
constructor(private $childProcess: IChildProcess,
24-
private $errors: IErrors,
20+
constructor(private $errors: IErrors,
2521
private $fs: IFileSystem,
26-
private $hostInfo: IHostInfo,
2722
private $logger: ILogger,
2823
private $options: IOptions,
2924
protected $staticConfig: Config.IStaticConfig) { }
@@ -50,39 +45,8 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
5045
this.showWarningsAsErrors = options && options.showWarningsAsErrors;
5146
const toolsInfoData = this.getToolsInfo();
5247
const isAndroidHomeValid = this.validateAndroidHomeEnvVariable();
53-
if (!toolsInfoData.compileSdkVersion) {
54-
this.printMessage(`Cannot find a compatible Android SDK for compilation. To be able to build for Android, install Android SDK ${AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET} or later.`,
55-
`Run \`\$ ${this.getPathToSdkManagementTool()}\` to manage your Android SDK versions.`);
56-
detectedErrors = true;
57-
}
58-
59-
if (!toolsInfoData.buildToolsVersion) {
60-
const buildToolsRange = this.getBuildToolsRange();
61-
const versionRangeMatches = buildToolsRange.match(/^.*?([\d\.]+)\s+.*?([\d\.]+)$/);
62-
let message = `You can install any version in the following range: '${buildToolsRange}'.`;
63-
64-
// Improve message in case buildToolsRange is something like: ">=22.0.0 <=22.0.0" - same numbers on both sides
65-
if (versionRangeMatches && versionRangeMatches[1] && versionRangeMatches[2] && versionRangeMatches[1] === versionRangeMatches[2]) {
66-
message = `You have to install version ${versionRangeMatches[1]}.`;
67-
}
68-
69-
let invalidBuildToolsAdditionalMsg = `Run \`\$ ${this.getPathToSdkManagementTool()}\` from your command-line to install required \`Android Build Tools\`.`;
70-
if (!isAndroidHomeValid) {
71-
invalidBuildToolsAdditionalMsg += ' In case you already have them installed, make sure `ANDROID_HOME` environment variable is set correctly.';
72-
}
7348

74-
this.printMessage("You need to have the Android SDK Build-tools installed on your system. " + message, invalidBuildToolsAdditionalMsg);
75-
detectedErrors = true;
76-
}
77-
78-
if (!toolsInfoData.supportRepositoryVersion) {
79-
let invalidSupportLibAdditionalMsg = `Run \`\$ ${this.getPathToSdkManagementTool()}\` to manage the Android Support Repository.`;
80-
if (!isAndroidHomeValid) {
81-
invalidSupportLibAdditionalMsg += ' In case you already have it installed, make sure `ANDROID_HOME` environment variable is set correctly.';
82-
}
83-
this.printMessage(`You need to have Android SDK ${AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET} or later and the latest Android Support Repository installed on your system.`, invalidSupportLibAdditionalMsg);
84-
detectedErrors = true;
85-
}
49+
detectedErrors = androidToolsInfo.validateInfo().map(warning => this.printMessage(warning.warning)).length > 0;
8650

8751
if (options && options.validateTargetSdk) {
8852
const targetSdk = toolsInfoData.targetSdkVersion;
@@ -104,44 +68,20 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
10468
}
10569

10670
public validateJavacVersion(installedJavacVersion: string, options?: { showWarningsAsErrors: boolean }): boolean {
107-
let hasProblemWithJavaVersion = false;
10871
if (options) {
10972
this.showWarningsAsErrors = options.showWarningsAsErrors;
11073
}
11174

112-
const additionalMessage = "You will not be able to build your projects for Android." + EOL
113-
+ "To be able to build for Android, verify that you have installed The Java Development Kit (JDK) and configured it according to system requirements as" + EOL +
114-
" described in " + this.$staticConfig.SYS_REQUIREMENTS_LINK;
115-
116-
const matchingVersion = appendZeroesToVersion(installedJavacVersion || "", 3).match(AndroidToolsInfo.VERSION_REGEX);
117-
const installedJavaCompilerVersion = matchingVersion && matchingVersion[1];
118-
if (installedJavaCompilerVersion) {
119-
if (semver.lt(installedJavaCompilerVersion, AndroidToolsInfo.MIN_JAVA_VERSION)) {
120-
hasProblemWithJavaVersion = true;
121-
this.printMessage(`Javac version ${installedJavacVersion} is not supported. You have to install at least ${AndroidToolsInfo.MIN_JAVA_VERSION}.`, additionalMessage);
122-
} else if (semver.gte(installedJavaCompilerVersion, AndroidToolsInfo.MAX_JAVA_VERSION)) {
123-
hasProblemWithJavaVersion = true;
124-
this.printMessage(`Javac version ${installedJavacVersion} is not supported. You have to install version ${AndroidToolsInfo.MIN_JAVA_VERSION}.`, additionalMessage);
125-
}
126-
} else {
127-
hasProblemWithJavaVersion = true;
128-
this.printMessage("Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable.", additionalMessage);
129-
}
130-
131-
return hasProblemWithJavaVersion;
75+
return androidToolsInfo.validateJavacVersion(installedJavacVersion).map(warning => this.printMessage(warning.warning)).length > 0;
13276
}
13377

13478
public async getPathToAdbFromAndroidHome(): Promise<string> {
135-
if (this.androidHome) {
136-
const pathToAdb = path.join(this.androidHome, "platform-tools", "adb");
137-
try {
138-
await this.$childProcess.execFile(pathToAdb, ["help"]);
139-
return pathToAdb;
140-
} catch (err) {
141-
// adb does not exist, so ANDROID_HOME is not set correctly
142-
// try getting default adb path (included in CLI package)
143-
this.$logger.trace(`Error while executing '${pathToAdb} help'. Error is: ${err.message}`);
144-
}
79+
try {
80+
return androidToolsInfo.getPathToAdbFromAndroidHome();
81+
} catch (err) {
82+
// adb does not exist, so ANDROID_HOME is not set correctly
83+
// try getting default adb path (included in CLI package)
84+
this.$logger.trace(`Error while executing '${path.join(this.androidHome, "platform-tools", "adb")} help'. Error is: ${err.message}`);
14585
}
14686

14787
return null;
@@ -153,43 +93,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
15393
this.showWarningsAsErrors = options.showWarningsAsErrors;
15494
}
15595

156-
const expectedDirectoriesInAndroidHome = ["build-tools", "tools", "platform-tools", "extras"];
157-
let androidHomeValidationResult = true;
158-
159-
if (!this.androidHome || !this.$fs.exists(this.androidHome)) {
160-
this.printMessage("The ANDROID_HOME environment variable is not set or it points to a non-existent directory. You will not be able to perform any build-related operations for Android.",
161-
"To be able to perform Android build-related operations, set the `ANDROID_HOME` variable to point to the root of your Android SDK installation directory.");
162-
androidHomeValidationResult = false;
163-
} else if (!_.some(expectedDirectoriesInAndroidHome.map(dir => this.$fs.exists(path.join(this.androidHome, dir))))) {
164-
this.printMessage("The ANDROID_HOME environment variable points to incorrect directory. You will not be able to perform any build-related operations for Android.",
165-
"To be able to perform Android build-related operations, set the `ANDROID_HOME` variable to point to the root of your Android SDK installation directory, " +
166-
"where you will find `tools` and `platform-tools` directories.");
167-
androidHomeValidationResult = false;
168-
}
169-
170-
return androidHomeValidationResult;
171-
}
172-
173-
@cache()
174-
private getPathToSdkManagementTool(): string {
175-
const sdkManagerName = "sdkmanager";
176-
let sdkManagementToolPath = sdkManagerName;
177-
178-
const isAndroidHomeValid = this.validateAndroidHomeEnvVariable();
179-
180-
if (isAndroidHomeValid) {
181-
// In case ANDROID_HOME is correct, check if sdkmanager exists and if not it means the SDK has not been updated.
182-
// In this case user shoud use `android` from the command-line instead of sdkmanager.
183-
const pathToSdkManager = path.join(this.androidHome, "tools", "bin", sdkManagerName);
184-
const pathToAndroidExecutable = path.join(this.androidHome, "tools", "android");
185-
const pathToExecutable = this.$fs.exists(pathToSdkManager) ? pathToSdkManager : pathToAndroidExecutable;
186-
187-
this.$logger.trace(`Path to Android SDK Management tool is: ${pathToExecutable}`);
188-
189-
sdkManagementToolPath = pathToExecutable.replace(this.androidHome, this.$hostInfo.isWindows ? "%ANDROID_HOME%" : "$ANDROID_HOME");
190-
}
191-
192-
return sdkManagementToolPath;
96+
return androidToolsInfo.validateAndroidHomeEnvVariable().map(warning => this.printMessage(warning.warning)).length > 0;
19397
}
19498

19599
private shouldGenerateTypings(): boolean {

lib/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,4 @@ $injector.requirePublic("extensibilityService", "./services/extensibility-servic
148148
$injector.require("nodeModulesDependenciesBuilder", "./tools/node-modules/node-modules-dependencies-builder");
149149
$injector.require("subscriptionService", "./services/subscription-service");
150150
$injector.require("terminalSpinnerService", "./services/terminal-spinner-service");
151+
$injector.require("platformEnvironmentRequirements", "./services/platform-environment-requirements");

lib/commands/build.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export class BuildCommandBase {
77
protected $platformsData: IPlatformsData,
88
protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
99
protected $platformService: IPlatformService,
10-
private $bundleValidatorHelper: IBundleValidatorHelper) {
10+
private $bundleValidatorHelper: IBundleValidatorHelper,
11+
private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements) {
12+
super($projectData, $errors, $options);
1113
this.$projectData.initializeProjectData();
1214
}
1315

@@ -44,7 +46,13 @@ export class BuildCommandBase {
4446
}
4547
}
4648

47-
protected validatePlatform(platform: string): void {
49+
protected async canExecuteCore(platform: string): Promise<boolean> {
50+
const result = await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({ platform: platform, cloudCommandName: "build" });
51+
this.validatePlatform(platform);
52+
return result;
53+
}
54+
55+
private validatePlatform(platform: string): void {
4856
if (!this.$platformService.isPlatformSupportedForOS(platform, this.$projectData)) {
4957
this.$errors.fail(`Applications for platform ${platform} can not be built on this OS`);
5058
}
@@ -62,17 +70,17 @@ export class BuildIosCommand extends BuildCommandBase implements ICommand {
6270
$platformsData: IPlatformsData,
6371
$devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
6472
$platformService: IPlatformService,
65-
$bundleValidatorHelper: IBundleValidatorHelper) {
66-
super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper);
73+
$bundleValidatorHelper: IBundleValidatorHelper,
74+
$platformEnvironmentRequirements: IPlatformEnvironmentRequirements) {
75+
super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper, $platformEnvironmentRequirements);
6776
}
6877

6978
public async execute(args: string[]): Promise<void> {
7079
return this.executeCore([this.$platformsData.availablePlatforms.iOS]);
7180
}
7281

73-
public canExecute(args: string[]): Promise<boolean> {
74-
super.validatePlatform(this.$devicePlatformsConstants.iOS);
75-
return args.length === 0 && this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS);
82+
public async canExecute(args: string[]): Promise<boolean> {
83+
return await super.canExecuteCore(this.$devicePlatformsConstants.iOS) && args.length === 0 && this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS);
7684
}
7785
}
7886

@@ -87,16 +95,18 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand {
8795
$platformsData: IPlatformsData,
8896
$devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
8997
$platformService: IPlatformService,
90-
$bundleValidatorHelper: IBundleValidatorHelper) {
91-
super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper);
98+
$bundleValidatorHelper: IBundleValidatorHelper,
99+
$platformEnvironmentRequirements: IPlatformEnvironmentRequirements) {
100+
super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper, $platformEnvironmentRequirements);
92101
}
93102

94103
public async execute(args: string[]): Promise<void> {
95104
return this.executeCore([this.$platformsData.availablePlatforms.Android]);
96105
}
97106

98107
public async canExecute(args: string[]): Promise<boolean> {
99-
super.validatePlatform(this.$devicePlatformsConstants.Android);
108+
await super.canExecuteCore(this.$devicePlatformsConstants.Android);
109+
100110
if (this.$options.release && (!this.$options.keyStorePath || !this.$options.keyStorePassword || !this.$options.keyStoreAlias || !this.$options.keyStoreAliasPassword)) {
101111
this.$errors.fail(ANDROID_RELEASE_BUILD_ERROR_MESSAGE);
102112
}

lib/commands/run.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ export class RunCommandBase implements ICommand {
99
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
1010
private $errors: IErrors,
1111
private $hostInfo: IHostInfo,
12-
private $liveSyncCommandHelper: ILiveSyncCommandHelper) { }
12+
private $liveSyncCommandHelper: ILiveSyncCommandHelper,
13+
private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements) { }
1314

1415
public allowedParameters: ICommandParameter[] = [];
1516
public async execute(args: string[]): Promise<void> {
1617
return this.$liveSyncCommandHelper.executeCommandLiveSync(this.platform);
1718
}
1819

1920
public async canExecute(args: string[]): Promise<boolean> {
21+
await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({platform: this.platform, cloudCommandName: "run"});
22+
2023
if (args.length) {
2124
this.$errors.fail(ERROR_NO_VALID_SUBCOMMAND_FORMAT, "run");
2225
}

lib/common

lib/definitions/platform.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,12 @@ interface IDeployPlatformInfo extends IPlatform, IAppFilesUpdaterOptionsComposit
365365
interface IUpdateAppOptions extends IOptionalFilesToSync, IOptionalFilesToRemove {
366366
beforeCopyAction: (sourceFiles: string[]) => void;
367367
}
368+
369+
interface IPlatformEnvironmentRequirements {
370+
checkEnvironmentRequirements(input: ICheckEnvironmentRequirementsInput): Promise<boolean>;
371+
}
372+
373+
interface ICheckEnvironmentRequirementsInput {
374+
platform: string;
375+
cloudCommandName: string;
376+
}

lib/services/doctor-service.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,38 @@ class DoctorService implements IDoctorService {
5353
return hasWarnings;
5454
}
5555

56+
public async runSetupScript(): Promise<ISpawnResult> {
57+
this.$logger.out("Running the setup script to try and automatically configure your environment.");
58+
59+
if (this.$hostInfo.isDarwin) {
60+
return this.runSetupScriptCore(DoctorService.DarwinSetupScriptLocation, []);
61+
}
62+
63+
if (this.$hostInfo.isWindows) {
64+
return this.runSetupScriptCore(DoctorService.WindowsSetupScriptExecutable, DoctorService.WindowsSetupScriptArguments);
65+
}
66+
}
67+
68+
public async canExecuteLocalBuild(platform?: string): Promise<boolean> {
69+
let infos = await doctor.getInfos();
70+
if (platform) {
71+
infos = this.filterInfosByPlatform(infos, platform);
72+
}
73+
this.printInfosCore(infos);
74+
75+
const warnings = this.filterInfosByType(infos, constants.WARNING_TYPE_NAME);
76+
return warnings.length === 0;
77+
}
78+
5679
private async promptForDocs(link: string): Promise<void> {
5780
if (await this.$prompter.confirm("Do you want to visit the official documentation?", () => helpers.isInteractive())) {
5881
this.$opener.open(link);
5982
}
6083
}
6184

62-
private async promptForHelpCore(link: string, commandName: string, commandArguments: string[]): Promise<void> {
63-
await this.promptForDocs(link);
64-
85+
private async promptForSetupScript(executablePath: string, setupScriptArgs: string[]): Promise<void> {
6586
if (await this.$prompter.confirm("Do you want to run the setup script?", () => helpers.isInteractive())) {
66-
await this.$childProcess.spawnFromEvent(commandName, commandArguments, "close", { stdio: "inherit" });
87+
await this.runSetupScriptCore(executablePath, setupScriptArgs);
6788
}
6889
}
6990

@@ -77,6 +98,15 @@ class DoctorService implements IDoctorService {
7798
}
7899
}
79100

101+
private async promptForHelpCore(link: string, setupScriptExecutablePath: string, setupScriptArgs: string[]): Promise<void> {
102+
await this.promptForDocs(link);
103+
await this.promptForSetupScript(setupScriptExecutablePath, setupScriptArgs);
104+
}
105+
106+
private async runSetupScriptCore(executablePath: string, setupScriptArgs: string[]): Promise<ISpawnResult> {
107+
return this.$childProcess.spawnFromEvent(executablePath, setupScriptArgs, "close", { stdio: "inherit" });
108+
}
109+
80110
private printPackageManagerTip() {
81111
if (this.$hostInfo.isWindows) {
82112
this.$logger.out("TIP: To avoid setting up the necessary environment variables, you can use the chocolatey package manager to install the Android SDK and its dependencies." + EOL);
@@ -98,7 +128,15 @@ class DoctorService implements IDoctorService {
98128
const spinner = this.$terminalSpinnerService.createSpinner();
99129
spinner.text = `${info.message.yellow} ${EOL} ${info.additionalInformation} ${EOL}`;
100130
spinner.fail();
101-
});
131+
});
132+
}
133+
134+
private filterInfosByPlatform(infos: NativeScriptDoctor.IInfo[], platform: string): NativeScriptDoctor.IInfo[] {
135+
return infos.filter(info => _.includes(info.platforms, platform));
136+
}
137+
138+
private filterInfosByType(infos: NativeScriptDoctor.IInfo[], type: string): NativeScriptDoctor.IInfo[] {
139+
return infos.filter(info => info.type === type);
102140
}
103141
}
104142
$injector.register("doctorService", DoctorService);

0 commit comments

Comments
 (0)