Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impr(android): allow gradle to automatically download missing dependencies #11432

Merged
merged 9 commits into from
Jan 30, 2020
237 changes: 64 additions & 173 deletions android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,30 +110,6 @@ AndroidBuilder.prototype.config = function config(logger, config, cli) {
_t.androidInfo = androidInfo;
assertIssue(logger, androidInfo.issues, 'ANDROID_JDK_NOT_FOUND');
assertIssue(logger, androidInfo.issues, 'ANDROID_JDK_PATH_CONTAINS_AMPERSANDS');
assertIssue(logger, androidInfo.issues, 'ANDROID_BUILD_TOOLS_CONFIG_SETTING_NOT_INSTALLED');
assertIssue(logger, androidInfo.issues, 'ANDROID_BUILD_TOOLS_TOO_NEW');
assertIssue(logger, androidInfo.issues, 'ANDROID_BUILD_TOOLS_NOT_SUPPORTED');

if (!cli.argv.prompt) {
// check that the Android SDK is found and sane
// note: if we're prompting, then we'll do this check in the --android-sdk validate() callback
assertIssue(logger, androidInfo.issues, 'ANDROID_SDK_NOT_FOUND');
assertIssue(logger, androidInfo.issues, 'ANDROID_SDK_MISSING_PROGRAMS');

// make sure we have an Android SDK and some Android targets
if (!Object.keys(androidInfo.targets).filter(function (id) {
var t = androidInfo.targets[id];
return t.type === 'platform' && t['api-level'] >= _t.minTargetApiLevel;
}).length) {
if (Object.keys(androidInfo.targets).length) {
logger.error(__('No valid Android SDK targets found.'));
} else {
logger.error(__('No Android SDK targets found.'));
}
logger.error(__('Please download an Android SDK target API level %s or newer from the Android SDK Manager and try again.', _t.minTargetApiLevel) + '\n');
process.exit(1);
}
}

// if --android-sdk was not specified, then we simply try to set a default android sdk
if (!cli.argv['android-sdk']) {
Expand Down Expand Up @@ -328,37 +304,36 @@ AndroidBuilder.prototype.config = function config(logger, config, cli) {
} else if (process.platform === 'win32' && value.indexOf('&') !== -1) {
callback(new Error(__('The Android SDK path cannot contain ampersands (&) on Windows')));
} else if (_t.androidInfo.sdk && _t.androidInfo.sdk.path === afs.resolvePath(value)) {
// no sense doing the detection again, just make sure we found the sdk
assertIssue(logger, _t.androidInfo.issues, 'ANDROID_SDK_NOT_FOUND');
assertIssue(logger, _t.androidInfo.issues, 'ANDROID_SDK_MISSING_PROGRAMS');
callback(null, value);
} else {
// do a quick scan to see if the path is correct
android.findSDK(value, config, appc.pkginfo.package(module), function (err) {
if (err) {
callback(new Error(__('Invalid Android SDK path: %s', value)));
} else {
function next() {
// set the android sdk in the config just in case a plugin or something needs it
config.set('android.sdkPath', value);

// path looks good, do a full scan again
androidDetect(config, { packageJson: _t.packageJson, bypassCache: true }, function (androidInfo) {
// check that the Android SDK is found and sane
assertIssue(logger, androidInfo.issues, 'ANDROID_SDK_NOT_FOUND');
assertIssue(logger, androidInfo.issues, 'ANDROID_SDK_MISSING_PROGRAMS');
_t.androidInfo = androidInfo;
callback(null, value);
});
}
// attempt to find android sdk
android.findSDK(value, config, appc.pkginfo.package(module), function () {

// new android sdk path looks good
// if we found an android sdk in the pre-validate hook, then we need to kill the other sdk's adb server
if (_t.androidInfo.sdk) {
new ADB(config).stopServer(next);
} else {
next();
}
// NOTE: ignore errors when finding sdk, let gradle validate the sdk

function next() {
// set the android sdk in the config just in case a plugin or something needs it
config.set('android.sdkPath', value);

// path looks good, do a full scan again
androidDetect(config, { packageJson: _t.packageJson, bypassCache: true }, function (androidInfo) {

// assume sdk is valid, let gradle validate the sdk
if (!androidInfo.sdk) {
androidInfo.sdk = { path: value };
}

_t.androidInfo = androidInfo;
callback(null, value);
});
}

// new android sdk path looks good
// if we found an android sdk in the pre-validate hook, then we need to kill the other sdk's adb server
if (_t.androidInfo.sdk) {
new ADB(config).stopServer(next);
} else {
next();
}
});
}
Expand Down Expand Up @@ -434,13 +409,14 @@ AndroidBuilder.prototype.config = function config(logger, config, cli) {
// if there are no devices/emulators, error
if (!Object.keys(opts).length) {
if (cli.argv.target === 'device') {
logger.error(__('Unable to find any devices') + '\n');
logger.log(__('Please plug in an Android device, then try again.') + '\n');
logger.warn(__('Unable to find any devices, possibily due to missing dependencies.') + '\n');
logger.log(__('Continuing with build... (will attempt to install missing dependencies)') + '\n');
} else {
logger.error(__('Unable to find any emulators') + '\n');
logger.log(__('Please create an Android emulator, then try again.') + '\n');
logger.warn(__('Unable to find any emulators, possibily due to missing dependencies.') + '\n');
logger.log(__('Continuing with build... (will attempt to install missing dependencies)') + '\n');
}
process.exit(1);
_t.buildOnly = true;
return callback();
}

callback(fields.select({
Expand Down Expand Up @@ -484,7 +460,7 @@ AndroidBuilder.prototype.config = function config(logger, config, cli) {
});
},
verifyIfRequired: function (callback) {
if (cli.argv['build-only']) {
if (_t.buildOnly) {
// not required if we're build only
return callback();
}
Expand Down Expand Up @@ -1040,7 +1016,13 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) {
}

// map sdk versions to sdk targets instead of by id
const targetSDKMap = {};
const targetSDKMap = {

// placeholder for gradle to use
[this.compileSdkVersion]: {
sdk: this.compileSdkVersion
}
};
Object.keys(this.androidInfo.targets).forEach(function (i) {
var t = this.androidInfo.targets[i];
if (t.type === 'platform') {
Expand All @@ -1050,11 +1032,6 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) {

// check the Android SDK we require to build exists
this.androidCompileSDK = targetSDKMap[this.compileSdkVersion];
if (!this.androidCompileSDK) {
logger.error(__('Unable to find Android SDK API %s', this.compileSdkVersion));
logger.error(__('Android SDK API %s is required to build Android apps', this.compileSdkVersion) + '\n');
process.exit(1);
}

// If "tiapp.xml" contains "AndroidManifest.xml" info, then load/store it to "this.customAndroidManifest" field.
try {
Expand Down Expand Up @@ -1184,77 +1161,17 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) {
}

} else {
// if no target sdk, then default to most recent supported/installed
Object
.keys(targetSDKMap)
.sort(function (a, b) {
if (targetSDKMap[a].sdk === targetSDKMap[b].sdk && targetSDKMap[a].revision === targetSDKMap[b].revision) {
return 0;
} else if (targetSDKMap[a].sdk < targetSDKMap[b].sdk || (targetSDKMap[a].sdk === targetSDKMap[b].sdk && targetSDKMap[a].revision < targetSDKMap[b].revision)) {
return -1;
}
return 1;
})
.reverse()
.some(function (ver) {
if (targetSDKMap[ver].sdk >= this.minTargetApiLevel && targetSDKMap[ver].sdk <= this.maxSupportedApiLevel) {
this.targetSDK = this.realTargetSDK = targetSDKMap[ver].sdk;
return true;
}
return false;
}, this);

if (!this.targetSDK || this.realTargetSDK < this.minTargetApiLevel) {
if (this.minTargetApiLevel === this.maxSupportedApiLevel) {
logger.error(__('Unable to find Android SDK API %s', this.maxSupportedApiLevel));
logger.error(__('Android SDK API %s is required to build Android apps', this.maxSupportedApiLevel) + '\n');
} else {
logger.error(__('Unable to find a suitable installed Android SDK that is API >=%s and <=%s', this.minTargetApiLevel, this.maxSupportedApiLevel) + '\n');
}
process.exit(1);
}
this.targetSDK = this.maxSupportedApiLevel;
this.realTargetSDK = this.targetSDK;
}

// check that we have this target sdk installed
this.androidTargetSDK = targetSDKMap[this.targetSDK];

if (!this.androidTargetSDK) {
logger.error(__('Target Android SDK API %s is not installed', this.targetSDK) + '\n');

const sdks = Object.keys(targetSDKMap).filter(function (ver) {
return ~~ver > this.minSupportedApiLevel;
}.bind(this)).sort().filter(function (s) { return s >= this.minSDK; }, this);

if (sdks.length) {
logger.log(__('To target Android SDK API %s, you first must install it using the Android SDK manager.', String(this.targetSDK).cyan) + '\n');
logger.log(
appc.string.wrap(
__('Alternatively, you can set the %s in the %s section of the tiapp.xml to one of the following installed Android target SDK APIs: %s', '<uses-sdk>'.cyan, '<android> <manifest>'.cyan, sdks.join(', ').cyan),
config.get('cli.width', 100)
)
);
logger.log();
logger.log('<ti:app xmlns:ti="http://ti.appcelerator.org">'.grey);
logger.log(' <android>'.grey);
logger.log(' <manifest>'.grey);
logger.log((' <uses-sdk '
+ (this.minSDK ? 'android:minSdkVersion="' + this.minSDK + '" ' : '')
+ 'android:targetSdkVersion="' + sdks[0] + '" '
+ (this.maxSDK ? 'android:maxSdkVersion="' + this.maxSDK + '" ' : '')
+ '/>').magenta);
logger.log(' </manifest>'.grey);
logger.log(' </android>'.grey);
logger.log('</ti:app>'.grey);
logger.log();
} else {
logger.log(__('To target Android SDK API %s, you first must install it using the Android SDK manager', String(this.targetSDK).cyan) + '\n');
}
process.exit(1);
}

if (!this.androidTargetSDK.androidJar) {
logger.error(__('Target Android SDK API %s is missing "android.jar"', this.targetSDK) + '\n');
process.exit(1);
this.androidTargetSDK = {
sdk: this.targetSDK
};
}

if (this.realTargetSDK < this.realMinSDK) {
Expand Down Expand Up @@ -1305,21 +1222,19 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) {

let deviceId = cli.argv['device-id'];

if (!cli.argv['build-only'] && /^device|emulator$/.test(this.target) && deviceId === undefined && config.get('android.autoSelectDevice', true)) {
if (!this.buildOnly && /^device|emulator$/.test(this.target) && deviceId === undefined && config.get('android.autoSelectDevice', true)) {
// no --device-id, so intelligently auto select one
const ver = this.androidTargetSDK.version,
apiLevel = this.androidTargetSDK.sdk,
devicesToAutoSelectFrom = this.devicesToAutoSelectFrom,
len = devicesToAutoSelectFrom.length,
verRegExp = /^((\d\.)?\d\.)?\d$/;
const apiLevel = this.androidTargetSDK.sdk,
devicesToAutoSelectFrom = this.devicesToAutoSelectFrom.sort((a, b) => b.api - a.api),
len = devicesToAutoSelectFrom.length;

// reset the device id
deviceId = null;

if (cli.argv.target === 'device') {
logger.info(__('Auto selecting device that closest matches %s', ver.cyan));
logger.info('Auto selecting device');
} else {
logger.info(__('Auto selecting emulator that closest matches %s', ver.cyan));
logger.info('Auto selecting emulator');
}

function setDeviceId(device) {
Expand All @@ -1339,68 +1254,44 @@ AndroidBuilder.prototype.validate = function validate(logger, config, cli) {
}
}

function gte(device) {
return device.api >= apiLevel && (!verRegExp.test(device.version) || appc.version.gte(device.version, ver));
}

function lt(device) {
return device.api < apiLevel && (!verRegExp.test(device.version) || appc.version.lt(device.version, ver));
}

// find the first one where version is >= and google apis == true
logger.debug(__('Searching for version >= %s and has Google APIs', ver));
logger.debug(__('Searching for API >= %s and has Google APIs', apiLevel.cyan));
for (let i = 0; i < len; i++) {
if (gte(devicesToAutoSelectFrom[i]) && devicesToAutoSelectFrom[i].googleApis) {
if (devicesToAutoSelectFrom[i].api >= apiLevel && devicesToAutoSelectFrom[i].googleApis) {
setDeviceId(devicesToAutoSelectFrom[i]);
break;
}
}

if (!deviceId) {
// find first one where version is >= and google apis is a maybe
logger.debug(__('Searching for version >= %s and may have Google APIs', ver));
logger.debug(__('Searching for API >= %s', apiLevel.cyan));
for (let i = 0; i < len; i++) {
if (gte(devicesToAutoSelectFrom[i]) && devicesToAutoSelectFrom[i].googleApis === null) {
if (devicesToAutoSelectFrom[i].api >= apiLevel) {
setDeviceId(devicesToAutoSelectFrom[i]);
break;
}
}

if (!deviceId) {
// find first one where version is >= and no google apis
logger.debug(__('Searching for version >= %s and no Google APIs', ver));
logger.debug(__('Searching for API < %s and has Google APIs', apiLevel.cyan));
for (let i = 0; i < len; i++) {
if (gte(devicesToAutoSelectFrom[i])) {
if (devicesToAutoSelectFrom[i].api < apiLevel && devicesToAutoSelectFrom[i].googleApis) { // eslint-disable-line max-depth
setDeviceId(devicesToAutoSelectFrom[i]);
break;
}
}

if (!deviceId) {
// find first one where version < and google apis == true
logger.debug(__('Searching for version < %s and has Google APIs', ver));
for (let i = len - 1; i >= 0; i--) {
if (lt(devicesToAutoSelectFrom[i])) { // eslint-disable-line max-depth
logger.debug(__('Searching for API < %s', apiLevel.cyan));
for (let i = 0; i < len; i++) { // eslint-disable-line max-depth
if (devicesToAutoSelectFrom[i].api < apiLevel) { // eslint-disable-line max-depth
setDeviceId(devicesToAutoSelectFrom[i]);
break;
}
}

if (!deviceId) {
// find first one where version <
logger.debug(__('Searching for version < %s and no Google APIs', ver));
for (let i = len - 1; i >= 0; i--) { // eslint-disable-line max-depth
if (lt(devicesToAutoSelectFrom[i]) && devicesToAutoSelectFrom[i].googleApis) { // eslint-disable-line max-depth
setDeviceId(devicesToAutoSelectFrom[i]);
break;
}
}

if (!deviceId) { // eslint-disable-line max-depth
// just grab first one
logger.debug(__('Selecting first device'));
setDeviceId(devicesToAutoSelectFrom[0]);
}
if (!deviceId) { // eslint-disable-line max-depth
logger.debug(__('Selecting first device'));
setDeviceId(devicesToAutoSelectFrom[0]);
}
}
}
Expand Down