From eb6ada80917e01e86d247409bfe8fbe5a60b4de8 Mon Sep 17 00:00:00 2001 From: filmaj Date: Mon, 13 Mar 2017 23:44:52 -0700 Subject: [PATCH] CB-12546: more robust sdk location detection. ANDROID_HOME now can be set from location of either of `adb`, `android` or `avdmanager` commands. slightly rework logic of infering ANDROID_HOME + setting up PATH to hopefully separate the logic into clearer sections. check_reqs.check_android now validates SDK Tools 25.3.1 binaries/structure. added specs for check_reqs.check_android. move android sdk version script. expose some helper functions as module methods to help with mocking. --- bin/android_sdk_version | 2 +- bin/lib/check_reqs.js | 86 +++++-- .../cordova}/lib/android_sdk.js | 0 bin/templates/cordova/lib/emulator.js | 12 +- spec/unit/check_reqs.spec.js | 214 ++++++++++++++++++ 5 files changed, 282 insertions(+), 32 deletions(-) rename bin/{ => templates/cordova}/lib/android_sdk.js (100%) create mode 100644 spec/unit/check_reqs.spec.js diff --git a/bin/android_sdk_version b/bin/android_sdk_version index 6140e11d..4d120c62 100755 --- a/bin/android_sdk_version +++ b/bin/android_sdk_version @@ -19,7 +19,7 @@ under the License. */ -var android_sdk_version = require('./lib/android_sdk'); +var android_sdk_version = require('./templates/cordova/lib/android_sdk'); android_sdk_version.run().done(null, function(err) { console.log(err); diff --git a/bin/lib/check_reqs.js b/bin/lib/check_reqs.js index b368eccf..6c93c4dc 100644 --- a/bin/lib/check_reqs.js +++ b/bin/lib/check_reqs.js @@ -30,7 +30,6 @@ var shelljs = require('shelljs'), ROOT = path.join(__dirname, '..', '..'); var CordovaError = require('cordova-common').CordovaError; -var isWindows = process.platform == 'win32'; function forgivingWhichSync(cmd) { try { @@ -51,6 +50,14 @@ function tryCommand(cmd, errMsg, catchStderr) { return d.promise; } +module.exports.isWindows = function() { + return (process.platform == 'win32'); +}; + +module.exports.isDarwin = function() { + return (process.platform == 'darwin'); +}; + // Get valid target from framework/project.properties module.exports.get_target = function() { function extractFromFile(filePath) { @@ -147,7 +154,7 @@ module.exports.check_java = function() { throw new CordovaError(msg); } } - } else if (isWindows) { + } else if (module.exports.isWindows()) { // Try to auto-detect java in the default install paths. var oldSilent = shelljs.config.silent; shelljs.config.silent = true; @@ -188,7 +195,8 @@ module.exports.check_java = function() { module.exports.check_android = function() { return Q().then(function() { var androidCmdPath = forgivingWhichSync('android'); - var adbInPath = !!forgivingWhichSync('adb'); + var adbInPath = forgivingWhichSync('adb'); + var avdmanagerInPath = forgivingWhichSync('avdmanager'); var hasAndroidHome = !!process.env['ANDROID_HOME'] && fs.existsSync(process.env['ANDROID_HOME']); function maybeSetAndroidHome(value) { if (!hasAndroidHome && fs.existsSync(value)) { @@ -196,8 +204,10 @@ module.exports.check_android = function() { process.env['ANDROID_HOME'] = value; } } - if (!hasAndroidHome && !androidCmdPath) { - if (isWindows) { + // First ensure ANDROID_HOME is set + // If we have no hints (nothing in PATH), try a few default locations + if (!hasAndroidHome && !androidCmdPath && !adbInPath && !avdmanagerInPath) { + if (module.exports.isWindows()) { // Android Studio 1.0 installer maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'sdk')); maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'sdk')); @@ -207,7 +217,7 @@ module.exports.check_android = function() { // Stand-alone installer maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-sdk')); maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-sdk')); - } else if (process.platform == 'darwin') { + } else if (module.exports.isDarwin()) { // Android Studio 1.0 installer maybeSetAndroidHome(path.join(process.env['HOME'], 'Library', 'Android', 'sdk')); // Android Studio pre-1.0 installer @@ -222,26 +232,42 @@ module.exports.check_android = function() { maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk')); } } - if (hasAndroidHome && !androidCmdPath) { - process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools'); - } - if (androidCmdPath && !hasAndroidHome) { - var parentDir = path.dirname(androidCmdPath); - var grandParentDir = path.dirname(parentDir); - if (path.basename(parentDir) == 'tools') { - process.env['ANDROID_HOME'] = path.dirname(parentDir); - hasAndroidHome = true; - } else if (fs.existsSync(path.join(grandParentDir, 'tools', 'android'))) { - process.env['ANDROID_HOME'] = grandParentDir; - hasAndroidHome = true; - } else { - throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + - 'Detected \'android\' command at ' + parentDir + ' but no \'tools\' directory found near.\n' + - 'Try reinstall Android SDK or update your PATH to include path to valid SDK directory.'); + if (!hasAndroidHome) { + // If we dont have ANDROID_HOME, but we do have some tools on the PATH, try to infer from the tooling PATH. + var parentDir, grandParentDir; + if (androidCmdPath) { + parentDir = path.dirname(androidCmdPath); + grandParentDir = path.dirname(parentDir); + if (path.basename(parentDir) == 'tools' || fs.existsSync(path.join(grandParentDir, 'tools', 'android'))) { + maybeSetAndroidHome(grandParentDir); + } else { + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + 'Detected \'android\' command at ' + parentDir + ' but no \'tools\' directory found near.\n' + + 'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'tools directory.'); + } + } + if (adbInPath) { + parentDir = path.dirname(adbInPath); + grandParentDir = path.dirname(parentDir); + if (path.basename(parentDir) == 'platform-tools') { + maybeSetAndroidHome(grandParentDir); + } else { + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + 'Detected \'adb\' command at ' + parentDir + ' but no \'platform-tools\' directory found near.\n' + + 'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'platform-tools directory.'); + } + } + if (avdmanagerInPath) { + parentDir = path.dirname(avdmanagerInPath); + grandParentDir = path.dirname(parentDir); + if (path.basename(parentDir) == 'bin' && path.basename(grandParentDir) == 'tools') { + maybeSetAndroidHome(path.dirname(grandParentDir)); + } else { + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + 'Detected \'avdmanager\' command at ' + parentDir + ' but no \'tools' + path.sep + 'bin\' directory found near.\n' + + 'Try reinstall Android SDK or update your PATH to include valid path to SDK' + path.sep + 'tools' + path.sep + 'bin directory.'); + } } - } - if (hasAndroidHome && !adbInPath) { - process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'platform-tools'); } if (!process.env['ANDROID_HOME']) { throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + @@ -251,6 +277,16 @@ module.exports.check_android = function() { throw new CordovaError('\'ANDROID_HOME\' environment variable is set to non-existent path: ' + process.env['ANDROID_HOME'] + '\nTry update it manually to point to valid SDK directory.'); } + // Next let's make sure relevant parts of the SDK tooling is in our PATH + if (hasAndroidHome && !androidCmdPath) { + process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools'); + } + if (hasAndroidHome && !adbInPath) { + process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'platform-tools'); + } + if (hasAndroidHome && !avdmanagerInPath) { + process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools', 'bin'); + } return hasAndroidHome; }); }; diff --git a/bin/lib/android_sdk.js b/bin/templates/cordova/lib/android_sdk.js similarity index 100% rename from bin/lib/android_sdk.js rename to bin/templates/cordova/lib/android_sdk.js diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js index 7a58479a..8554fe9b 100644 --- a/bin/templates/cordova/lib/emulator.js +++ b/bin/templates/cordova/lib/emulator.js @@ -30,6 +30,7 @@ var events = require('cordova-common').events; var spawn = require('cordova-common').superspawn.spawn; var CordovaError = require('cordova-common').CordovaError; var shelljs = require('shelljs'); +var android_sdk = require('./android_sdk'); var Q = require('q'); var os = require('os'); @@ -52,7 +53,7 @@ function forgivingWhichSync(cmd) { } } -function list_images_using_avdmanager() { +module.exports.list_images_using_avdmanager = function () { return spawn('avdmanager', ['list', 'avd']) .then(function(output) { var response = output.split('\n'); @@ -90,7 +91,6 @@ function list_images_using_avdmanager() { } var version_string = img_obj['target'].replace(/Android\s+/, ''); - var android_sdk = require('./android_sdk'); var api_level = android_sdk.version_string_to_api_level[version_string]; if (api_level) { img_obj['target'] += ' (API level ' + api_level + ')'; @@ -112,7 +112,7 @@ function list_images_using_avdmanager() { } return emulator_list; }); -} +}; /** * Returns a Promise for a list of emulator images in the form of objects @@ -166,14 +166,14 @@ module.exports.list_images = function() { } return emulator_list; - }).catch(function(stderr) { + }).catch(function(err) { // try to use `avdmanager` in case `android` has problems // this likely means the target machine is using a newer version of // the android sdk, and possibly `avdmanager` is available. - return list_images_using_avdmanager(); + return module.exports.list_images_using_avdmanager(); }); } else if (forgivingWhichSync('avdmanager')) { - return list_images_using_avdmanager(); + return module.exports.list_images_using_avdmanager(); } else { return Q().then(function() { throw new CordovaError('Could not find either `android` or `avdmanager` on your $PATH! Are you sure the Android SDK is installed and available?'); diff --git a/spec/unit/check_reqs.spec.js b/spec/unit/check_reqs.spec.js new file mode 100644 index 00000000..1ddc3528 --- /dev/null +++ b/spec/unit/check_reqs.spec.js @@ -0,0 +1,214 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +/* jshint laxcomma:true */ + +var check_reqs = require("../../bin/lib/check_reqs"); +var shelljs = require("shelljs"); +var fs = require("fs"); +var path = require("path"); + +describe("check_reqs", function () { + var original_env; + beforeAll(function() { + original_env = Object.create(process.env); + }); + afterEach(function() { + Object.keys(original_env).forEach(function(k) { + process.env[k] = original_env[k]; + }); + }); + describe("check_android", function() { + describe("set ANDROID_HOME if not set", function() { + beforeEach(function() { + delete process.env.ANDROID_HOME; + }); + describe("even if no Android binaries are on the PATH", function() { + beforeEach(function() { + spyOn(shelljs, "which").and.returnValue(null); + spyOn(fs, "existsSync").and.returnValue(true); + }); + it("it should set ANDROID_HOME on Windows", function(done) { + spyOn(check_reqs, "isWindows").and.returnValue(true); + process.env.LOCALAPPDATA = "windows-local-app-data"; + process.env.ProgramFiles = "windows-program-files"; + return check_reqs.check_android() + .then(function() { + expect(process.env.ANDROID_HOME).toContain("windows-local-app-data"); + }).fail(function(err) { + expect(err).toBeUndefined(); + console.log(err); + }).fin(function() { + delete process.env.LOCALAPPDATA; + delete process.env.ProgramFiles; + done(); + }); + }); + it("it should set ANDROID_HOME on Darwin", function(done) { + spyOn(check_reqs, "isWindows").and.returnValue(false); + spyOn(check_reqs, "isDarwin").and.returnValue(true); + process.env.HOME = "home is where the heart is"; + return check_reqs.check_android() + .then(function() { + expect(process.env.ANDROID_HOME).toContain("home is where the heart is"); + }).fail(function(err) { + expect(err).toBeUndefined(); + console.log(err); + }).fin(function() { + delete process.env.HOME; + done(); + }); + }); + }); + describe("if some Android tooling exists on the PATH", function() { + beforeEach(function() { + spyOn(fs, "realpathSync").and.callFake(function(path) { + return path; + }); + }); + it("should set ANDROID_HOME based on `android` command if command exists in a SDK-like directory structure", function(done) { + spyOn(fs, "existsSync").and.returnValue(true); + spyOn(shelljs, "which").and.callFake(function(cmd) { + if (cmd == "android") { + return "/android/sdk/tools/android"; + } else { + return null; + } + }); + return check_reqs.check_android() + .then(function() { + expect(process.env.ANDROID_HOME).toEqual("/android/sdk"); + done(); + }).fail(function(err) { + expect(err).toBeUndefined(); + console.log(err); + }); + }); + it("should error out if `android` command exists in a non-SDK-like directory structure", function(done) { + spyOn(shelljs, "which").and.callFake(function(cmd) { + if (cmd == "android") { + return "/just/some/random/path/android"; + } else { + return null; + } + }); + return check_reqs.check_android() + .then(function() { + done.fail(); + }).fail(function(err) { + expect(err).toBeDefined(); + expect(err.message).toContain("update your PATH to include valid path"); + done(); + }); + }); + it("should set ANDROID_HOME based on `adb` command if command exists in a SDK-like directory structure", function(done) { + spyOn(fs, "existsSync").and.returnValue(true); + spyOn(shelljs, "which").and.callFake(function(cmd) { + if (cmd == "adb") { + return "/android/sdk/platform-tools/adb"; + } else { + return null; + } + }); + return check_reqs.check_android() + .then(function() { + expect(process.env.ANDROID_HOME).toEqual("/android/sdk"); + done(); + }).fail(function(err) { + expect(err).toBeUndefined(); + console.log(err); + }); + }); + it("should error out if `adb` command exists in a non-SDK-like directory structure", function(done) { + spyOn(shelljs, "which").and.callFake(function(cmd) { + if (cmd == "adb") { + return "/just/some/random/path/adb"; + } else { + return null; + } + }); + return check_reqs.check_android() + .then(function() { + done.fail(); + }).fail(function(err) { + expect(err).toBeDefined(); + expect(err.message).toContain("update your PATH to include valid path"); + done(); + }); + }); + it("should set ANDROID_HOME based on `avdmanager` command if command exists in a SDK-like directory structure", function(done) { + spyOn(fs, "existsSync").and.returnValue(true); + spyOn(shelljs, "which").and.callFake(function(cmd) { + if (cmd == "avdmanager") { + return "/android/sdk/tools/bin/avdmanager"; + } else { + return null; + } + }); + return check_reqs.check_android() + .then(function() { + expect(process.env.ANDROID_HOME).toEqual("/android/sdk"); + done(); + }).fail(function(err) { + expect(err).toBeUndefined(); + console.log(err); + }); + }); + it("should error out if `avdmanager` command exists in a non-SDK-like directory structure", function(done) { + spyOn(shelljs, "which").and.callFake(function(cmd) { + if (cmd == "avdmanager") { + return "/just/some/random/path/avdmanager"; + } else { + return null; + } + }); + return check_reqs.check_android() + .then(function() { + done.fail(); + }).fail(function(err) { + expect(err).toBeDefined(); + expect(err.message).toContain("update your PATH to include valid path"); + done(); + }); + }); + }); + }); + describe("set PATH for various Android binaries if not available", function() { + beforeEach(function() { + spyOn(shelljs, "which").and.returnValue(null); + process.env.ANDROID_HOME = "let the children play"; + spyOn(fs, "existsSync").and.returnValue(true); + }); + afterEach(function() { + delete process.env.ANDROID_HOME; + }); + it("should add tools/bin,tools,platform-tools to PATH if `avdmanager`,`android`,`adb` is not found", function(done) { + return check_reqs.check_android() + .then(function() { + expect(process.env.PATH).toContain("let the children play" + path.sep + "tools"); + expect(process.env.PATH).toContain("let the children play" + path.sep + "platform-tools"); + expect(process.env.PATH).toContain("let the children play" + path.sep + "tools" + path.sep + "bin"); + done(); + }).fail(function(err) { + expect(err).toBeUndefined(); + console.log(err); + }); + }); + }); + }); +});