diff --git a/docs/man_pages/cloud/cloud-setup.md b/docs/man_pages/cloud/cloud-setup.md new file mode 100644 index 0000000000..4c7225bb00 --- /dev/null +++ b/docs/man_pages/cloud/cloud-setup.md @@ -0,0 +1,18 @@ +<% if (isJekyll) { %>--- +title: tns cloud setup +position: 5 +---<% } %> +# tns cloud setup +========== + +Usage | Synopsis +------|------- +Install the `nativescript-cloud extension` | `$ tns cloud setup` + +Install the `nativescript-cloud extension` to configure your environment for cloud builds. + +### Related Commands + +Command | Description +----------|---------- +[setup](setup.html) | Run the setup script to try to automatically configure your environment for local builds. \ No newline at end of file diff --git a/docs/man_pages/env-configuration/setup.md b/docs/man_pages/env-configuration/setup.md new file mode 100644 index 0000000000..de90c80f85 --- /dev/null +++ b/docs/man_pages/env-configuration/setup.md @@ -0,0 +1,18 @@ +<% if (isJekyll) { %>--- +title: tns setup +position: 5 +---<% } %> +# tns setup +========== + +Usage | Synopsis +------|------- +Run the setup script | `$ tns setup` + +Run the setup script to try to automatically configure your environment for local builds. + +### Related Commands + +Command | Description +----------|---------- +[setup cloud](setup-cloud.html) | Install the nativescript-cloud extension to configure your environment for cloud builds. \ No newline at end of file diff --git a/docs/man_pages/index.md b/docs/man_pages/index.md index 0602ad3101..54c6218068 100644 --- a/docs/man_pages/index.md +++ b/docs/man_pages/index.md @@ -57,6 +57,12 @@ Command | Description [device run](device/device-run.html) | Runs the selected application on a connected device. [device list-applications](device/device-list-applications.html) | Lists the installed applications on all connected devices. +## Environment Configuration Commands +Command | Description +---|--- +[setup](env-configuration/setup.html) | Run the setup script to try to automatically configure your environment for local builds. +[setup cloud](cloud/cloud-setup.html) | Install the `nativescript-cloud extension` to configure your environment for cloud builds. + ## Global Options Option | Description -------|--------- diff --git a/lib/android-tools-info.ts b/lib/android-tools-info.ts index 51e988be7d..cd9cdbe0fd 100644 --- a/lib/android-tools-info.ts +++ b/lib/android-tools-info.ts @@ -1,8 +1,7 @@ import * as path from "path"; import * as semver from "semver"; -import { EOL } from "os"; import { cache } from "./common/decorators"; -import { appendZeroesToVersion } from './common/helpers'; +import { androidToolsInfo } from "nativescript-doctor"; export class AndroidToolsInfo implements IAndroidToolsInfo { private static ANDROID_TARGET_PREFIX = "android"; @@ -10,8 +9,6 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { private static MIN_REQUIRED_COMPILE_TARGET = 22; private static REQUIRED_BUILD_TOOLS_RANGE_PREFIX = ">=23"; private static VERSION_REGEX = /((\d+\.){2}\d+)/; - private static MIN_JAVA_VERSION = "1.8.0"; - private static MAX_JAVA_VERSION = "1.9.0"; private showWarningsAsErrors: boolean; private toolsInfo: IAndroidToolsInfoData; @@ -20,10 +17,8 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { return process.env["ANDROID_HOME"]; } - constructor(private $childProcess: IChildProcess, - private $errors: IErrors, + constructor(private $errors: IErrors, private $fs: IFileSystem, - private $hostInfo: IHostInfo, private $logger: ILogger, private $options: IOptions, protected $staticConfig: Config.IStaticConfig) { } @@ -45,151 +40,69 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { return this.toolsInfo; } - public validateInfo(options?: { showWarningsAsErrors: boolean, validateTargetSdk: boolean }): boolean { + public validateInfo(options?: IAndroidToolsInfoValidateInput): boolean { let detectedErrors = false; this.showWarningsAsErrors = options && options.showWarningsAsErrors; - const toolsInfoData = this.getToolsInfo(); const isAndroidHomeValid = this.validateAndroidHomeEnvVariable(); - if (!toolsInfoData.compileSdkVersion) { - 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.`, - `Run \`\$ ${this.getPathToSdkManagementTool()}\` to manage your Android SDK versions.`); - detectedErrors = true; - } - if (!toolsInfoData.buildToolsVersion) { - const buildToolsRange = this.getBuildToolsRange(); - const versionRangeMatches = buildToolsRange.match(/^.*?([\d\.]+)\s+.*?([\d\.]+)$/); - let message = `You can install any version in the following range: '${buildToolsRange}'.`; - - // Improve message in case buildToolsRange is something like: ">=22.0.0 <=22.0.0" - same numbers on both sides - if (versionRangeMatches && versionRangeMatches[1] && versionRangeMatches[2] && versionRangeMatches[1] === versionRangeMatches[2]) { - message = `You have to install version ${versionRangeMatches[1]}.`; - } + detectedErrors = androidToolsInfo.validateInfo().map(warning => this.printMessage(warning.warning)).length > 0; - let invalidBuildToolsAdditionalMsg = `Run \`\$ ${this.getPathToSdkManagementTool()}\` from your command-line to install required \`Android Build Tools\`.`; - if (!isAndroidHomeValid) { - invalidBuildToolsAdditionalMsg += ' In case you already have them installed, make sure `ANDROID_HOME` environment variable is set correctly.'; - } - - this.printMessage("You need to have the Android SDK Build-tools installed on your system. " + message, invalidBuildToolsAdditionalMsg); - detectedErrors = true; + if (options && options.validateTargetSdk) { + detectedErrors = this.validateTargetSdk(); } - if (!toolsInfoData.supportRepositoryVersion) { - let invalidSupportLibAdditionalMsg = `Run \`\$ ${this.getPathToSdkManagementTool()}\` to manage the Android Support Repository.`; - if (!isAndroidHomeValid) { - invalidSupportLibAdditionalMsg += ' In case you already have it installed, make sure `ANDROID_HOME` environment variable is set correctly.'; - } - 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); - detectedErrors = true; - } + return detectedErrors || !isAndroidHomeValid; + } - if (options && options.validateTargetSdk) { - const targetSdk = toolsInfoData.targetSdkVersion; - const newTarget = `${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-${targetSdk}`; - if (!_.includes(AndroidToolsInfo.SUPPORTED_TARGETS, newTarget)) { - const supportedVersions = AndroidToolsInfo.SUPPORTED_TARGETS.sort(); - const minSupportedVersion = this.parseAndroidSdkString(_.first(supportedVersions)); - - if (targetSdk && (targetSdk < minSupportedVersion)) { - this.printMessage(`The selected Android target SDK ${newTarget} is not supported. You must target ${minSupportedVersion} or later.`); - detectedErrors = true; - } else if (!targetSdk || targetSdk > this.getMaxSupportedVersion()) { - this.$logger.warn(`Support for the selected Android target SDK ${newTarget} is not verified. Your Android app might not work as expected.`); - } + public validateTargetSdk(options?: IAndroidToolsInfoOptions): boolean { + this.showWarningsAsErrors = options && options.showWarningsAsErrors; + + const toolsInfoData = this.getToolsInfo(); + const targetSdk = toolsInfoData.targetSdkVersion; + const newTarget = `${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-${targetSdk}`; + + if (!_.includes(AndroidToolsInfo.SUPPORTED_TARGETS, newTarget)) { + const supportedVersions = AndroidToolsInfo.SUPPORTED_TARGETS.sort(); + const minSupportedVersion = this.parseAndroidSdkString(_.first(supportedVersions)); + + if (targetSdk && (targetSdk < minSupportedVersion)) { + this.printMessage(`The selected Android target SDK ${newTarget} is not supported. You must target ${minSupportedVersion} or later.`); + return true; + } else if (!targetSdk || targetSdk > this.getMaxSupportedVersion()) { + this.$logger.warn(`Support for the selected Android target SDK ${newTarget} is not verified. Your Android app might not work as expected.`); } } - return detectedErrors || !isAndroidHomeValid; + return false; } - public validateJavacVersion(installedJavacVersion: string, options?: { showWarningsAsErrors: boolean }): boolean { - let hasProblemWithJavaVersion = false; + public validateJavacVersion(installedJavacVersion: string, options?: IAndroidToolsInfoOptions): boolean { if (options) { this.showWarningsAsErrors = options.showWarningsAsErrors; } - const additionalMessage = "You will not be able to build your projects for Android." + EOL - + "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 + - " described in " + this.$staticConfig.SYS_REQUIREMENTS_LINK; - - const matchingVersion = appendZeroesToVersion(installedJavacVersion || "", 3).match(AndroidToolsInfo.VERSION_REGEX); - const installedJavaCompilerVersion = matchingVersion && matchingVersion[1]; - if (installedJavaCompilerVersion) { - if (semver.lt(installedJavaCompilerVersion, AndroidToolsInfo.MIN_JAVA_VERSION)) { - hasProblemWithJavaVersion = true; - this.printMessage(`Javac version ${installedJavacVersion} is not supported. You have to install at least ${AndroidToolsInfo.MIN_JAVA_VERSION}.`, additionalMessage); - } else if (semver.gte(installedJavaCompilerVersion, AndroidToolsInfo.MAX_JAVA_VERSION)) { - hasProblemWithJavaVersion = true; - this.printMessage(`Javac version ${installedJavacVersion} is not supported. You have to install version ${AndroidToolsInfo.MIN_JAVA_VERSION}.`, additionalMessage); - } - } else { - hasProblemWithJavaVersion = true; - this.printMessage("Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable.", additionalMessage); - } - - return hasProblemWithJavaVersion; + return androidToolsInfo.validateJavacVersion(installedJavacVersion).map(warning => this.printMessage(warning.warning)).length > 0; } public async getPathToAdbFromAndroidHome(): Promise { - if (this.androidHome) { - const pathToAdb = path.join(this.androidHome, "platform-tools", "adb"); - try { - await this.$childProcess.execFile(pathToAdb, ["help"]); - return pathToAdb; - } catch (err) { - // adb does not exist, so ANDROID_HOME is not set correctly - // try getting default adb path (included in CLI package) - this.$logger.trace(`Error while executing '${pathToAdb} help'. Error is: ${err.message}`); - } + try { + return androidToolsInfo.getPathToAdbFromAndroidHome(); + } catch (err) { + // adb does not exist, so ANDROID_HOME is not set correctly + // try getting default adb path (included in CLI package) + this.$logger.trace(`Error while executing '${path.join(this.androidHome, "platform-tools", "adb")} help'. Error is: ${err.message}`); } return null; } @cache() - public validateAndroidHomeEnvVariable(options?: { showWarningsAsErrors: boolean }): boolean { + public validateAndroidHomeEnvVariable(options?: IAndroidToolsInfoOptions): boolean { if (options) { this.showWarningsAsErrors = options.showWarningsAsErrors; } - const expectedDirectoriesInAndroidHome = ["build-tools", "tools", "platform-tools", "extras"]; - let androidHomeValidationResult = true; - - if (!this.androidHome || !this.$fs.exists(this.androidHome)) { - 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.", - "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."); - androidHomeValidationResult = false; - } else if (!_.some(expectedDirectoriesInAndroidHome.map(dir => this.$fs.exists(path.join(this.androidHome, dir))))) { - this.printMessage("The ANDROID_HOME environment variable points to incorrect directory. You will not be able to perform any build-related operations for Android.", - "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, " + - "where you will find `tools` and `platform-tools` directories."); - androidHomeValidationResult = false; - } - - return androidHomeValidationResult; - } - - @cache() - private getPathToSdkManagementTool(): string { - const sdkManagerName = "sdkmanager"; - let sdkManagementToolPath = sdkManagerName; - - const isAndroidHomeValid = this.validateAndroidHomeEnvVariable(); - - if (isAndroidHomeValid) { - // In case ANDROID_HOME is correct, check if sdkmanager exists and if not it means the SDK has not been updated. - // In this case user shoud use `android` from the command-line instead of sdkmanager. - const pathToSdkManager = path.join(this.androidHome, "tools", "bin", sdkManagerName); - const pathToAndroidExecutable = path.join(this.androidHome, "tools", "android"); - const pathToExecutable = this.$fs.exists(pathToSdkManager) ? pathToSdkManager : pathToAndroidExecutable; - - this.$logger.trace(`Path to Android SDK Management tool is: ${pathToExecutable}`); - - sdkManagementToolPath = pathToExecutable.replace(this.androidHome, this.$hostInfo.isWindows ? "%ANDROID_HOME%" : "$ANDROID_HOME"); - } - - return sdkManagementToolPath; + return androidToolsInfo.validateAndroidHomeEnvVariable().map(warning => this.printMessage(warning.warning)).length > 0; } private shouldGenerateTypings(): boolean { diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 4dd9ec2c74..6711c80bab 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -72,12 +72,15 @@ $injector.requireCommand("appstore|upload", "./commands/appstore-upload"); $injector.requireCommand("publish|ios", "./commands/appstore-upload"); $injector.require("itmsTransporterService", "./services/itmstransporter-service"); +$injector.requireCommand("setup|*", "./commands/setup"); +$injector.requireCommand(["setup|cloud", "cloud|setup"], "./commands/setup"); + $injector.requirePublic("npm", "./node-package-manager"); $injector.require("npmInstallationManager", "./npm-installation-manager"); $injector.require("dynamicHelpProvider", "./dynamic-help-provider"); $injector.require("mobilePlatformsCapabilities", "./mobile-platforms-capabilities"); $injector.require("commandsServiceProvider", "./providers/commands-service-provider"); -$injector.require("deviceAppDataProvider", "./providers/device-app-data-provider"); +$injector.require("AppDataProvider", "./providers/device-app-data-provider"); $injector.require("deviceLogProvider", "./common/mobile/device-log-provider"); $injector.require("projectFilesProvider", "./providers/project-files-provider"); @@ -152,3 +155,5 @@ $injector.require("subscriptionService", "./services/subscription-service"); $injector.require("terminalSpinnerService", "./services/terminal-spinner-service"); $injector.require('playgroundService', './services/playground-service'); +$injector.require("platformEnvironmentRequirements", "./services/platform-environment-requirements"); +$injector.require("nativescriptCloudExtensionService", "./services/nativescript-cloud-extension-service"); diff --git a/lib/commands/build.ts b/lib/commands/build.ts index e87fd05edb..fbee66cf3b 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -8,7 +8,7 @@ export class BuildCommandBase { protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, protected $platformService: IPlatformService, private $bundleValidatorHelper: IBundleValidatorHelper) { - this.$projectData.initializeProjectData(); + this.$projectData.initializeProjectData(); } public async executeCore(args: string[]): Promise { @@ -44,12 +44,16 @@ export class BuildCommandBase { } } - protected validatePlatform(platform: string): void { + protected async validatePlatform(platform: string): Promise { if (!this.$platformService.isPlatformSupportedForOS(platform, this.$projectData)) { this.$errors.fail(`Applications for platform ${platform} can not be built on this OS`); } this.$bundleValidatorHelper.validate(); + + const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); + const platformProjectService = platformData.platformProjectService; + await platformProjectService.validate(this.$projectData); } } @@ -63,15 +67,15 @@ export class BuildIosCommand extends BuildCommandBase implements ICommand { $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $platformService: IPlatformService, $bundleValidatorHelper: IBundleValidatorHelper) { - super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper); + super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper); } public async execute(args: string[]): Promise { return this.executeCore([this.$platformsData.availablePlatforms.iOS]); } - public canExecute(args: string[]): Promise { - super.validatePlatform(this.$devicePlatformsConstants.iOS); + public async canExecute(args: string[]): Promise { + await super.validatePlatform(this.$devicePlatformsConstants.iOS); return args.length === 0 && this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS); } } @@ -88,7 +92,7 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, $platformService: IPlatformService, $bundleValidatorHelper: IBundleValidatorHelper) { - super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper); + super($options, $errors, $projectData, $platformsData, $devicePlatformsConstants, $platformService, $bundleValidatorHelper); } public async execute(args: string[]): Promise { @@ -96,15 +100,11 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { } public async canExecute(args: string[]): Promise { - super.validatePlatform(this.$devicePlatformsConstants.Android); + await super.validatePlatform(this.$devicePlatformsConstants.Android); if (this.$options.release && (!this.$options.keyStorePath || !this.$options.keyStorePassword || !this.$options.keyStoreAlias || !this.$options.keyStoreAliasPassword)) { this.$errors.fail(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); } - const platformData = this.$platformsData.getPlatformData(this.$devicePlatformsConstants.Android, this.$projectData); - const platformProjectService = platformData.platformProjectService; - await platformProjectService.validate(this.$projectData); - return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.Android); } } diff --git a/lib/commands/setup.ts b/lib/commands/setup.ts new file mode 100644 index 0000000000..928582fb3e --- /dev/null +++ b/lib/commands/setup.ts @@ -0,0 +1,21 @@ +export class SetupCommand implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor(private $doctorService: IDoctorService) { } + + public execute(args: string[]): Promise { + return this.$doctorService.runSetupScript(); + } +} +$injector.registerCommand("setup|*", SetupCommand); + +export class CloudSetupCommand implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor(private $nativescriptCloudExtensionService: INativescriptCloudExtensionService) { } + + public execute(args: string[]): Promise { + return this.$nativescriptCloudExtensionService.install(); + } +} +$injector.registerCommand(["setup|cloud", "cloud|setup"], CloudSetupCommand); diff --git a/lib/common b/lib/common index 90db0aa6d9..f978eaf4bc 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 90db0aa6d98c9b105d2c9a982cbcbb53b375e32e +Subproject commit f978eaf4bcec7e6564246616dae2de809504efb2 diff --git a/lib/constants.ts b/lib/constants.ts index 2f4cc82782..3857c72542 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -137,3 +137,5 @@ export const enum BuildStates { Clean = "Clean", Incremental = "Incremental" } + +export const NATIVESCRIPT_CLOUD_EXTENSION_NAME = "nativescript-cloud"; diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index be53399b4e..b3774f131f 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -547,7 +547,7 @@ interface IAndroidToolsInfo { * @param {any} options Defines if the warning messages should treated as error and if the targetSdk value should be validated as well. * @return {boolean} True if there are detected issues, false otherwise. */ - validateInfo(options?: { showWarningsAsErrors: boolean, validateTargetSdk: boolean }): boolean; + validateInfo(options?: IAndroidToolsInfoValidateInput): boolean; /** * Validates the information about required JAVA version. @@ -555,14 +555,21 @@ interface IAndroidToolsInfo { * @param {any} options Defines if the warning messages should treated as error. * @return {boolean} True if there are detected issues, false otherwise. */ - validateJavacVersion(installedJavaVersion: string, options?: { showWarningsAsErrors: boolean }): boolean; + validateJavacVersion(installedJavaVersion: string, options?: IAndroidToolsInfoOptions): boolean; /** * Validates if ANDROID_HOME environment variable is set correctly. * @param {any} options Defines if the warning messages should treated as error. * @returns {boolean} true in case ANDROID_HOME is correctly set, false otherwise. */ - validateAndroidHomeEnvVariable(options?: { showWarningsAsErrors: boolean }): boolean; + validateAndroidHomeEnvVariable(options?: IAndroidToolsInfoOptions): boolean; + + /** + * Validates target sdk + * @param {IAndroidToolsInfoOptions} options @optional Defines if the warning messages should treated as error. + * @returns {boolean} True if there are detected issues, false otherwise + */ + validateTargetSdk(options?: IAndroidToolsInfoOptions): boolean; /** * Gets the path to `adb` executable from ANDROID_HOME. It should be `$ANDROID_HOME/platform-tools/adb` in case it exists. @@ -607,6 +614,23 @@ interface IAndroidToolsInfoData { generateTypings: boolean; } +/** + * Describes options that can be passed to methods from IAndroidToolsInfo interface + */ +interface IAndroidToolsInfoOptions { + /** + * Defines if the warning messages should treated as error. + */ + showWarningsAsErrors: boolean; +} + +interface IAndroidToolsInfoValidateInput extends IAndroidToolsInfoOptions { + /** + * Defines if the targetSdk value should be validated. + */ + validateTargetSdk: boolean; +} + interface ISocketProxyFactory extends NodeJS.EventEmitter { createTCPSocketProxy(factory: () => Promise): Promise; createWebSocketProxy(factory: () => Promise, deviceIdentifier: string): Promise; @@ -762,3 +786,11 @@ interface IBundleValidatorHelper { */ validate(): void; } + +interface INativescriptCloudExtensionService { + /** + * Installs nativescript-cloud extension + * @return {Promise} returns the extension data + */ + install(): Promise; +} diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 3edeb6af49..2cc11f6942 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -365,3 +365,7 @@ interface IDeployPlatformInfo extends IPlatform, IAppFilesUpdaterOptionsComposit interface IUpdateAppOptions extends IOptionalFilesToSync, IOptionalFilesToRemove { beforeCopyAction: (sourceFiles: string[]) => void; } + +interface IPlatformEnvironmentRequirements { + checkEnvironmentRequirements(platform: string): Promise; +} \ No newline at end of file diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 442c40cfd7..3330b6944a 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -32,12 +32,12 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private $hostInfo: IHostInfo, private $logger: ILogger, $projectDataService: IProjectDataService, - private $sysInfo: ISysInfo, private $injector: IInjector, private $pluginVariablesService: IPluginVariablesService, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $npm: INodePackageManager, - private $androidPluginBuildService: IAndroidPluginBuildService) { + private $androidPluginBuildService: IAndroidPluginBuildService, + private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements) { super($fs, $projectDataService); this._androidProjectPropertiesManagers = Object.create(null); this.isAndroidStudioTemplate = false; @@ -155,13 +155,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject this.validatePackageName(projectData.projectId); this.validateProjectName(projectData.projectName); - this.$androidToolsInfo.validateAndroidHomeEnvVariable({ showWarningsAsErrors: true }); - - const javaCompilerVersion = await this.$sysInfo.getJavaCompilerVersion(); - - this.$androidToolsInfo.validateJavacVersion(javaCompilerVersion, { showWarningsAsErrors: true }); - - await this.$androidToolsInfo.validateInfo({ showWarningsAsErrors: true, validateTargetSdk: true }); + await this.$platformEnvironmentRequirements.checkEnvironmentRequirements(this.getPlatformData(projectData).normalizedPlatformName); + this.$androidToolsInfo.validateTargetSdk({ showWarningsAsErrors: true }); } public async validatePlugins(): Promise { diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 36679d0e03..b77c59b289 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -53,17 +53,42 @@ class DoctorService implements IDoctorService { return hasWarnings; } + public runSetupScript(): Promise { + if (this.$hostInfo.isLinux) { + return; + } + + this.$logger.out("Running the setup script to try and automatically configure your environment."); + + if (this.$hostInfo.isDarwin) { + return this.runSetupScriptCore(DoctorService.DarwinSetupScriptLocation, []); + } + + if (this.$hostInfo.isWindows) { + return this.runSetupScriptCore(DoctorService.WindowsSetupScriptExecutable, DoctorService.WindowsSetupScriptArguments); + } + } + + public async canExecuteLocalBuild(platform?: string): Promise { + let infos = await doctor.getInfos(); + if (platform) { + infos = this.filterInfosByPlatform(infos, platform); + } + this.printInfosCore(infos); + + const warnings = this.filterInfosByType(infos, constants.WARNING_TYPE_NAME); + return warnings.length === 0; + } + private async promptForDocs(link: string): Promise { if (await this.$prompter.confirm("Do you want to visit the official documentation?", () => helpers.isInteractive())) { this.$opener.open(link); } } - private async promptForHelpCore(link: string, commandName: string, commandArguments: string[]): Promise { - await this.promptForDocs(link); - + private async promptForSetupScript(executablePath: string, setupScriptArgs: string[]): Promise { if (await this.$prompter.confirm("Do you want to run the setup script?", () => helpers.isInteractive())) { - await this.$childProcess.spawnFromEvent(commandName, commandArguments, "close", { stdio: "inherit" }); + await this.runSetupScriptCore(executablePath, setupScriptArgs); } } @@ -77,6 +102,15 @@ class DoctorService implements IDoctorService { } } + private async promptForHelpCore(link: string, setupScriptExecutablePath: string, setupScriptArgs: string[]): Promise { + await this.promptForDocs(link); + await this.promptForSetupScript(setupScriptExecutablePath, setupScriptArgs); + } + + private async runSetupScriptCore(executablePath: string, setupScriptArgs: string[]): Promise { + return this.$childProcess.spawnFromEvent(executablePath, setupScriptArgs, "close", { stdio: "inherit" }); + } + private printPackageManagerTip() { if (this.$hostInfo.isWindows) { 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); @@ -86,6 +120,16 @@ class DoctorService implements IDoctorService { } private printInfosCore(infos: NativeScriptDoctor.IInfo[]): void { + if (!helpers.isInteractive()) { + infos.map(info => { + let message = info.message; + if (info.type === constants.WARNING_TYPE_NAME) { + message = `WARNING: ${info.message.yellow} ${EOL} ${info.additionalInformation} ${EOL}`; + } + this.$logger.out(message); + }); + } + infos.filter(info => info.type === constants.INFO_TYPE_NAME) .map(info => { const spinner = this.$terminalSpinnerService.createSpinner(); @@ -98,7 +142,15 @@ class DoctorService implements IDoctorService { const spinner = this.$terminalSpinnerService.createSpinner(); spinner.text = `${info.message.yellow} ${EOL} ${info.additionalInformation} ${EOL}`; spinner.fail(); - }); + }); + } + + private filterInfosByPlatform(infos: NativeScriptDoctor.IInfo[], platform: string): NativeScriptDoctor.IInfo[] { + return infos.filter(info => _.includes(info.platforms, platform)); + } + + private filterInfosByType(infos: NativeScriptDoctor.IInfo[], type: string): NativeScriptDoctor.IInfo[] { + return infos.filter(info => info.type === type); } } $injector.register("doctorService", DoctorService); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index bc579b7143..4e92d9d7ac 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -49,9 +49,10 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private $pbxprojDomXcode: IPbxprojDomXcode, private $xcode: IXcode, private $iOSEntitlementsService: IOSEntitlementsService, + private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, private $sysInfo: ISysInfo, private $xCConfigService: XCConfigService) { - super($fs, $projectDataService); + super($fs, $projectDataService); } private _platformsDirCache: string = null; @@ -130,16 +131,12 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName, "Resources"); } - public async validate(): Promise { + public async validate(projectData: IProjectData): Promise { if (!this.$hostInfo.isDarwin) { return; } - try { - await this.$childProcess.exec("which xcodebuild"); - } catch (error) { - this.$errors.fail("Xcode is not installed. Make sure you have Xcode installed and added to your PATH"); - } + await this.$platformEnvironmentRequirements.checkEnvironmentRequirements(this.getPlatformData(projectData).normalizedPlatformName); const xcodeBuildVersion = await this.getXcodeVersion(); if (helpers.versionCompare(xcodeBuildVersion, IOSProjectService.XCODEBUILD_MIN_VERSION) < 0) { diff --git a/lib/services/nativescript-cloud-extension-service.ts b/lib/services/nativescript-cloud-extension-service.ts new file mode 100644 index 0000000000..ef33a8d84b --- /dev/null +++ b/lib/services/nativescript-cloud-extension-service.ts @@ -0,0 +1,17 @@ +import * as constants from "../constants"; + +export class NativescriptCloudExtensionService implements INativescriptCloudExtensionService { + + constructor(private $extensibilityService: IExtensibilityService, + private $logger: ILogger) { } + + public install(): Promise { + const installedExtensions = this.$extensibilityService.getInstalledExtensions(); + if (!installedExtensions[constants.NATIVESCRIPT_CLOUD_EXTENSION_NAME]) { + return this.$extensibilityService.installExtension(constants.NATIVESCRIPT_CLOUD_EXTENSION_NAME); + } + + this.$logger.out(`Extension ${constants.NATIVESCRIPT_CLOUD_EXTENSION_NAME} is already installed.`); + } +} +$injector.register("nativescriptCloudExtensionService", NativescriptCloudExtensionService); diff --git a/lib/services/platform-environment-requirements.ts b/lib/services/platform-environment-requirements.ts new file mode 100644 index 0000000000..11ae785f18 --- /dev/null +++ b/lib/services/platform-environment-requirements.ts @@ -0,0 +1,136 @@ +import * as constants from "../constants"; +import { isInteractive } from "../common/helpers"; +import { EOL } from "os"; + +export class PlatformEnvironmentRequirements implements IPlatformEnvironmentRequirements { + constructor(private $commandsService: ICommandsService, + private $doctorService: IDoctorService, + private $errors: IErrors, + private $nativescriptCloudExtensionService: INativescriptCloudExtensionService, + private $logger: ILogger, + private $prompter: IPrompter, + private $staticConfig: IStaticConfig) { } + + public static CLOUD_BUILDS_OPTION_NAME = "Configure for Cloud Builds"; + public static SETUP_SCRIPT_OPTION_NAME = "Configure for Local Builds"; + public static MANUALLY_SETUP_OPTION_NAME = "Skip Step and Configure Manually"; + private static BOTH_CLOUD_BUILDS_AND_SETUP_SCRIPT_OPTION_NAME = "Configure for Both Local and Cloud Builds"; + private static NOT_CONFIGURED_ENV_MESSAGE = "To continue, choose one of the following options: "; + private static NOT_CONFIGURED_ENV_AFTER_SETUP_SCRIPT_MESSAGE = "The setup script was not able to configure your environment for local builds. To execute local builds, you have to set up your environment manually. To continue, choose one of the following options:"; + + private cliCommandToCloudCommandName: IStringDictionary = { + "build": "tns cloud build", + "run": "tns cloud run", + "deploy": "tns cloud deploy" + }; + + public async checkEnvironmentRequirements(platform: string): Promise { + const canExecute = await this.$doctorService.canExecuteLocalBuild(platform); + if (!canExecute) { + if (!isInteractive()) { + this.fail(`You are missing the ${constants.NATIVESCRIPT_CLOUD_EXTENSION_NAME} extension and you will not be able to execute cloud builds. Your environment is not configured properly and you will not be able to execute local builds. To continue, choose one of the following options: ` + EOL + + "Run $ tns setup command to run the setup script to try to automatically configure your environment for local builds." + EOL + + `Run $ tns cloud setup command to install the ${constants.NATIVESCRIPT_CLOUD_EXTENSION_NAME} extension to configure your environment for cloud builds.` + EOL + + `Verify that your environment is configured according to the system requirements described at ${this.$staticConfig.SYS_REQUIREMENTS_LINK}`); + } + + this.$logger.info(`You are missing the ${constants.NATIVESCRIPT_CLOUD_EXTENSION_NAME} extension and you will not be able to execute cloud builds. Your environment is not configured properly and you will not be able to execute local builds. ` + EOL + + `Select "Configure for Cloud Builds" to install the ${constants.NATIVESCRIPT_CLOUD_EXTENSION_NAME} extension and automatically configure your environment for cloud builds.` + EOL + + `Select "Configure for Local Builds" to run the setup script and automatically configure your environment for local builds.` + + `Select "Configure for Both Local and Cloud Builds" to automatically configure your environment for both options.` + + `Select "Skip Step and Configure Manually" to disregard these options and install any required components manually.`); + + const selectedOption = await this.$prompter.promptForChoice(PlatformEnvironmentRequirements.NOT_CONFIGURED_ENV_MESSAGE, [ + PlatformEnvironmentRequirements.CLOUD_BUILDS_OPTION_NAME, + PlatformEnvironmentRequirements.SETUP_SCRIPT_OPTION_NAME, + PlatformEnvironmentRequirements.BOTH_CLOUD_BUILDS_AND_SETUP_SCRIPT_OPTION_NAME, + PlatformEnvironmentRequirements.MANUALLY_SETUP_OPTION_NAME, + ]); + + await this.processCloudBuildsIfNeeded(platform, selectedOption); + + this.processManuallySetupIfNeeded(platform, selectedOption); + + if (selectedOption === PlatformEnvironmentRequirements.SETUP_SCRIPT_OPTION_NAME) { + await this.$doctorService.runSetupScript(); + + if (await this.$doctorService.canExecuteLocalBuild(platform)) { + return true; + } + + const option = await this.$prompter.promptForChoice(PlatformEnvironmentRequirements.NOT_CONFIGURED_ENV_AFTER_SETUP_SCRIPT_MESSAGE, [ + PlatformEnvironmentRequirements.CLOUD_BUILDS_OPTION_NAME, + PlatformEnvironmentRequirements.MANUALLY_SETUP_OPTION_NAME + ]); + + await this.processCloudBuildsIfNeeded(platform, option); + + this.processManuallySetupIfNeeded(platform, option); + } + + if (selectedOption === PlatformEnvironmentRequirements.BOTH_CLOUD_BUILDS_AND_SETUP_SCRIPT_OPTION_NAME) { + await this.processBothCloudBuildsAndSetupScript(platform); + if (await this.$doctorService.canExecuteLocalBuild(platform)) { + return true; + } + + this.processManuallySetup(platform); + } + } + + return true; + } + + private async processCloudBuildsIfNeeded(platform: string, selectedOption: string): Promise { + if (selectedOption === PlatformEnvironmentRequirements.CLOUD_BUILDS_OPTION_NAME) { + await this.processCloudBuilds(platform); + } + } + + private async processCloudBuilds(platform: string): Promise { + await this.processCloudBuildsCore(platform); + this.fail(this.getCloudBuildsMessage(platform)); + } + + private processCloudBuildsCore(platform: string): Promise { + return this.$nativescriptCloudExtensionService.install(); + } + + private getCloudBuildsMessage(platform: string): string { + const cloudCommandName = this.cliCommandToCloudCommandName[this.$commandsService.currentCommandData.commandName]; + if (!cloudCommandName) { + return `In order to test your application use the $ tns login command to log in with your account and then $ tns cloud build command to build your app in the cloud.`; + } + + if (!platform) { + return `Use the $ tns login command to log in with your account and then $ ${cloudCommandName.toLowerCase()} command.`; + } + + return `Use the $ tns login command to log in with your account and then $ ${cloudCommandName.toLowerCase()} ${platform.toLowerCase()} command.`; + } + + private processManuallySetupIfNeeded(platform: string, selectedOption: string) { + if (selectedOption === PlatformEnvironmentRequirements.MANUALLY_SETUP_OPTION_NAME) { + this.processManuallySetup(platform); + } + } + + private processManuallySetup(platform: string): void { + this.fail(`To be able to build for ${platform}, verify that your environment is configured according to the system requirements described at ${this.$staticConfig.SYS_REQUIREMENTS_LINK}`); + } + + private async processBothCloudBuildsAndSetupScript(platform: string): Promise { + try { + await this.processCloudBuildsCore(platform); + } catch (e) { + this.$logger.trace(`Error while installing ${constants.NATIVESCRIPT_CLOUD_EXTENSION_NAME} extension. ${e.message}.`); + } + + await this.$doctorService.runSetupScript(); + } + + private fail(message: string): void { + this.$errors.fail({ formatStr: message, suppressCommandHelp: true, printOnStdout: true }); + } +} +$injector.register("platformEnvironmentRequirements", PlatformEnvironmentRequirements); diff --git a/test/android-tools-info.ts b/test/android-tools-info.ts index f548462455..d8fb69c732 100644 --- a/test/android-tools-info.ts +++ b/test/android-tools-info.ts @@ -1,24 +1,13 @@ import { Yok } from "../lib/common/yok"; import { AndroidToolsInfo } from "../lib/android-tools-info"; -import { EOL } from "os"; import { format } from "util"; import { assert } from "chai"; -interface ITestData { - javacVersion: string; - expectedResult: boolean; - warnings?: string[]; -} - describe("androidToolsInfo", () => { let loggedWarnings: string[] = []; let loggedMarkdownMessages: string[] = []; const sysRequirementsLink = ""; - const additionalMsg = "You will not be able to build your projects for Android." + EOL - + "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 + - " described in " + sysRequirementsLink; - beforeEach(() => { loggedWarnings = []; loggedMarkdownMessages = []; @@ -52,57 +41,6 @@ describe("androidToolsInfo", () => { }; describe("validateJavacVersion", () => { - const testData: ITestData[] = [ - { - javacVersion: "1.8.0", - expectedResult: false - }, - { - javacVersion: "1.8.0_152", - expectedResult: false - }, - { - javacVersion: "9", - expectedResult: true, - warnings: ["Javac version 9 is not supported. You have to install version 1.8.0."] - }, - { - javacVersion: "9.0.1", - expectedResult: true, - warnings: ["Javac version 9.0.1 is not supported. You have to install version 1.8.0."] - }, - { - javacVersion: "1.7.0", - expectedResult: true, - warnings: ["Javac version 1.7.0 is not supported. You have to install at least 1.8.0."] - }, - { - javacVersion: "1.7.0_132", - expectedResult: true, - warnings: ["Javac version 1.7.0_132 is not supported. You have to install at least 1.8.0."] - }, - { - javacVersion: null, - expectedResult: true, - warnings: ["Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable."] - } - ]; - - _.each(testData, ({ javacVersion, expectedResult, warnings }) => { - it(`returns ${expectedResult} when version is ${javacVersion}`, () => { - const testInjector = createTestInjector(); - const androidToolsInfo = testInjector.resolve(AndroidToolsInfo); - assert.deepEqual(androidToolsInfo.validateJavacVersion(javacVersion), expectedResult); - if (warnings && warnings.length) { - assert.deepEqual(loggedWarnings, warnings); - assert.deepEqual(loggedMarkdownMessages, [additionalMsg]); - } else { - assert.equal(loggedWarnings.length, 0); - assert.equal(loggedMarkdownMessages.length, 0); - } - }); - }); - it("throws error when passing showWarningsAsErrors to true and javac is not installed", () => { const testInjector = createTestInjector(); const androidToolsInfo = testInjector.resolve(AndroidToolsInfo); diff --git a/test/debug.ts b/test/debug.ts index 067f81f580..8a892e6c7e 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -77,6 +77,7 @@ function createTestInjector(): IInjector { }); testInjector.register("settingsService", SettingsService); testInjector.register("androidPluginBuildService", stubs.AndroidPluginBuildServiceStub); + testInjector.register("platformEnvironmentRequirements", {}); return testInjector; } diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index 673944c74a..3e383a35fb 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -118,6 +118,7 @@ function createTestInjector(projectPath: string, projectName: string): IInjector testInjector.register("xCConfigService", XCConfigService); testInjector.register("settingsService", SettingsService); testInjector.register("httpClient", {}); + testInjector.register("platformEnvironmentRequirements", {}); return testInjector; } diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 7329920c54..9fe3d0e1bd 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -117,6 +117,7 @@ function createTestInjector() { getPlaygroundInfo: () => Promise.resolve(null) }); + testInjector.register("platformEnvironmentRequirements", {}); return testInjector; } diff --git a/test/services/platform-environment-requirements.ts b/test/services/platform-environment-requirements.ts new file mode 100644 index 0000000000..8c52e7635e --- /dev/null +++ b/test/services/platform-environment-requirements.ts @@ -0,0 +1,195 @@ +import { Yok } from "../../lib/common/yok"; +import { PlatformEnvironmentRequirements } from '../../lib/services/platform-environment-requirements'; +import * as stubs from "../stubs"; +import { assert } from "chai"; + +const platform = "android"; +const cloudBuildsErrorMessage = `In order to test your application use the $ tns login command to log in with your account and then $ tns cloud build command to build your app in the cloud.`; +const manuallySetupErrorMessage = `To be able to build for ${platform}, verify that your environment is configured according to the system requirements described at `; +const nonInteractiveConsoleErrorMessage = `You are missing the nativescript-cloud extension and you will not be able to execute cloud builds. Your environment is not configured properly and you will not be able to execute local builds. To continue, choose one of the following options: \nRun $ tns setup command to run the setup script to try to automatically configure your environment for local builds.\nRun $ tns cloud setup command to install the nativescript-cloud extension to configure your environment for cloud builds.\nVerify that your environment is configured according to the system requirements described at `; + +function createTestInjector() { + const testInjector = new Yok(); + + testInjector.register("commandsService", {currentCommandData: {commandName: "test", commandArguments: [""]}}); + testInjector.register("doctorService", {}); + testInjector.register("errors", { + fail: (err: any) => { + throw new Error(err.formatStr || err.message || err); + } + }); + testInjector.register("logger", stubs.LoggerStub); + testInjector.register("prompter", {}); + testInjector.register("platformEnvironmentRequirements", PlatformEnvironmentRequirements); + testInjector.register("staticConfig", { SYS_REQUIREMENTS_LINK: "" }); + testInjector.register("nativescriptCloudExtensionService", {}); + + return testInjector; +} + +describe("platformEnvironmentRequirements ", () => { + describe("checkRequirements", () => { + let testInjector: IInjector = null; + let platformEnvironmentRequirements: IPlatformEnvironmentRequirements = null; + + beforeEach(() => { + testInjector = createTestInjector(); + platformEnvironmentRequirements = testInjector.resolve("platformEnvironmentRequirements"); + process.stdout.isTTY = true; + process.stdin.isTTY = true; + }); + + it("should show prompt when environment is not configured", async () => { + const doctorService = testInjector.resolve("doctorService"); + doctorService.canExecuteLocalBuild = () => false; + + let isPromptForChoiceCalled = false; + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = () => { + isPromptForChoiceCalled = true; + return PlatformEnvironmentRequirements.CLOUD_BUILDS_OPTION_NAME; + }; + + let isInstallExtensionCalled = false; + const nativescriptCloudExtensionService = testInjector.resolve("nativescriptCloudExtensionService"); + nativescriptCloudExtensionService.install = () => {isInstallExtensionCalled = true; }; + + await assert.isRejected(platformEnvironmentRequirements.checkEnvironmentRequirements(platform)); + assert.isTrue(isPromptForChoiceCalled); + assert.isTrue(isInstallExtensionCalled); + }); + + it("should return true when environment is configured", async () => { + const doctorService = testInjector.resolve("doctorService"); + doctorService.canExecuteLocalBuild = () => true; + + const result = await platformEnvironmentRequirements.checkEnvironmentRequirements(platform); + assert.isTrue(result); + }); + + describe("when setup script option is selected ", () => { + it("should return true when env is configured after executing setup script", async () => { + const doctorService = testInjector.resolve("doctorService"); + doctorService.canExecuteLocalBuild = () => false; + doctorService.runSetupScript = async () => { doctorService.canExecuteLocalBuild = () => true; }; + + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = () => Promise.resolve(PlatformEnvironmentRequirements.SETUP_SCRIPT_OPTION_NAME); + + const result = await platformEnvironmentRequirements.checkEnvironmentRequirements(platform); + assert.isTrue(result); + }); + it("should prompt for choice when env is not configured after executing setup script", async () => { + const doctorService = testInjector.resolve("doctorService"); + doctorService.canExecuteLocalBuild = () => false; + doctorService.runSetupScript = () => Promise.resolve(); + + let isPromptForChoiceCalled = true; + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = () => { + if (isPromptForChoiceCalled) { + isPromptForChoiceCalled = false; + return PlatformEnvironmentRequirements.SETUP_SCRIPT_OPTION_NAME; + } + + isPromptForChoiceCalled = true; + return PlatformEnvironmentRequirements.CLOUD_BUILDS_OPTION_NAME; + }; + + let isInstallExtensionCalled = false; + const nativescriptCloudExtensionService = testInjector.resolve("nativescriptCloudExtensionService"); + nativescriptCloudExtensionService.install = () => {isInstallExtensionCalled = true; }; + + await assert.isRejected(platformEnvironmentRequirements.checkEnvironmentRequirements(platform)); + assert.isTrue(isInstallExtensionCalled); + assert.isTrue(isPromptForChoiceCalled); + }); + + describe("and environment is not configured after executing setup script ", () => { + beforeEach(() => { + const doctorService = testInjector.resolve("doctorService"); + doctorService.canExecuteLocalBuild = () => false; + doctorService.runSetupScript = () => Promise.resolve(); + }); + it("should install nativescript-cloud extension when cloud builds option is selected", async () => { + let isPromptForChoiceCalled = true; + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = () => { + if (isPromptForChoiceCalled) { + isPromptForChoiceCalled = false; + return PlatformEnvironmentRequirements.SETUP_SCRIPT_OPTION_NAME; + } + + isPromptForChoiceCalled = true; + return PlatformEnvironmentRequirements.CLOUD_BUILDS_OPTION_NAME; + }; + + let isInstallExtensionCalled = false; + const nativescriptCloudExtensionService = testInjector.resolve("nativescriptCloudExtensionService"); + nativescriptCloudExtensionService.install = () => {isInstallExtensionCalled = true; }; + + await assert.isRejected(platformEnvironmentRequirements.checkEnvironmentRequirements(platform), cloudBuildsErrorMessage); + assert.isTrue(isInstallExtensionCalled); + assert.isTrue(isPromptForChoiceCalled); + }); + it("should fail when manually setup option is selected", async () => { + let isPromptForChoiceCalled = true; + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = () => { + if (isPromptForChoiceCalled) { + isPromptForChoiceCalled = false; + return PlatformEnvironmentRequirements.SETUP_SCRIPT_OPTION_NAME; + } + + isPromptForChoiceCalled = true; + return PlatformEnvironmentRequirements.MANUALLY_SETUP_OPTION_NAME; + }; + + await assert.isRejected(platformEnvironmentRequirements.checkEnvironmentRequirements(platform), manuallySetupErrorMessage); + assert.isTrue(isPromptForChoiceCalled); + }); + }); + }); + + describe("when cloud builds option is selected", () => { + it("should install nativescript-cloud extension when cloud builds option is selected", async () => { + const doctorService = testInjector.resolve("doctorService"); + doctorService.canExecuteLocalBuild = () => false; + + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = () => Promise.resolve(PlatformEnvironmentRequirements.CLOUD_BUILDS_OPTION_NAME); + + let isInstallExtensionCalled = false; + const nativescriptCloudExtensionService = testInjector.resolve("nativescriptCloudExtensionService"); + nativescriptCloudExtensionService.install = () => {isInstallExtensionCalled = true; }; + + await assert.isRejected(platformEnvironmentRequirements.checkEnvironmentRequirements(platform), cloudBuildsErrorMessage); + assert.isTrue(isInstallExtensionCalled); + }); + }); + + describe("when manually setup option is selected", () => { + it("should fail when manually setup option is selected", async () => { + const doctorService = testInjector.resolve("doctorService"); + doctorService.canExecuteLocalBuild = () => false; + + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = () => PlatformEnvironmentRequirements.MANUALLY_SETUP_OPTION_NAME; + + await assert.isRejected(platformEnvironmentRequirements.checkEnvironmentRequirements(platform), manuallySetupErrorMessage); + }); + }); + + describe("when console is not interactive", () => { + it("should fail when console is not interactive", async () => { + process.stdout.isTTY = false; + process.stdin.isTTY = false; + + const doctorService = testInjector.resolve("doctorService"); + doctorService.canExecuteLocalBuild = () => false; + + await assert.isRejected(platformEnvironmentRequirements.checkEnvironmentRequirements(platform), nonInteractiveConsoleErrorMessage); + }); + }); + }); +}); diff --git a/test/stubs.ts b/test/stubs.ts index c1e26bf437..9e36009616 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -606,6 +606,10 @@ export class AndroidToolsInfoStub implements IAndroidToolsInfo { public validateAndroidHomeEnvVariable(options?: { showWarningsAsErrors: boolean }): boolean { return false; } + + public validateTargetSdk(options?: { showWarningsAsErrors: boolean }): boolean { + return true; + } } export class ChildProcessStub { @@ -655,6 +659,8 @@ export class ProjectChangesService implements IProjectChangesService { } export class CommandsService implements ICommandsService { + public currentCommandData = { commandName: "test", commandArguments: [""]}; + public allCommands(opts: { includeDevCommands: boolean }): string[] { return []; }