Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions lib/check_reqs.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const java = require('./env/java');
const { CordovaError, ConfigParser, events } = require('cordova-common');
const android_sdk = require('./android_sdk');
const { SDK_VERSION } = require('./gradle-config-defaults');
const AndroidCommandLineTools = require('./env/AndroidCommandLineTools');

// Re-exporting these for backwards compatibility and for unit testing.
// TODO: Remove uses and use the ./utils module directly.
Expand Down Expand Up @@ -216,14 +217,14 @@ module.exports.check_android = function () {
}
}
if (avdmanagerInPath) {
parentDir = path.dirname(avdmanagerInPath);
grandParentDir = path.dirname(parentDir);
if (path.basename(parentDir) === 'bin' && path.basename(grandParentDir) === 'tools') {
maybeSetAndroidHome(path.dirname(grandParentDir));
let sdkPath = null;
if (/cmdline-tools/.test(avdmanagerInPath)) {
sdkPath = path.resolve(avdmanagerInPath, '../../../..');
maybeSetAndroidHome(sdkPath);
} else {
throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try 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.');
'Detected \'avdmanager\' command at ' + parentDir + ' but does not appear to be within an Android SDK installation.\n' +
'Try reinstall Android SDK or update your PATH to include valid path to SDK');
}
}
}
Expand All @@ -240,7 +241,10 @@ module.exports.check_android = function () {
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');
const cmdLineToolsBin = AndroidCommandLineTools.getBinPath();
if (cmdLineToolsBin) {
process.env.PATH += path.delimiter + cmdLineToolsBin;
}
}
return hasAndroidHome;
});
Expand Down
102 changes: 102 additions & 0 deletions lib/env/AndroidCommandLineTools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
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.
*/

const { events } = require('cordova-common');
const fs = require('node:fs');
const path = require('node:path');
const semver = require('semver');

/**
* Utility collection for resolving the Android SDK command line tools installed
* on the workstation.
*/
const AndroidCommandLineTools = {
/**
* Gets a sorted list of available versions found on the system.
*
* If the command line tools is not resolvable, then an empty array will be returned.
*
* This function depends on ANDROID_HOME environment variable.
*
* @returns {String[]}
*/
getAvailableVersions: () => {
const androidHome = path.resolve(AndroidCommandLineTools.__getAndroidHome());

if (!fs.existsSync(androidHome)) {
events.emit('warn', 'ANDROID_HOME is not resolvable.');
return [];
}

const cmdLineToolsContainer = path.join(androidHome, 'cmdline-tools');
if (!fs.existsSync(cmdLineToolsContainer)) {
events.emit('warn', 'Android SDK is missing cmdline-tools directory.');
return [];
}

const cmdLineVersions = fs.readdirSync(cmdLineToolsContainer)
.filter((value) => {
// expected directory paths are semver-like version strings or literally "latest"
return value === 'latest' || semver.coerce(value) !== null;
})
.sort((a, b) => {
// "latest" directory always comes first
if (a === 'latest') return -1;
if (b === 'latest') return 1;

const av = semver.coerce(a, {
includePrerelease: true
});
const bv = semver.coerce(b, {
includePrerelease: true
});

// Descending (highest version first)
return semver.rcompare(av, bv);
});

return cmdLineVersions;
},

/**
* Gets the bin path of the cmd line tools using the latest available that
* is installed on the workstation.
*
* Returns null if there are no versions fond
*
* @returns {String | null}
*/
getBinPath: () => {
const versions = AndroidCommandLineTools.getAvailableVersions();

if (versions.length === 0) {
return null;
}

const version = versions[0];
return path.resolve(AndroidCommandLineTools.__getAndroidHome(), 'cmdline-tools', version, 'bin');
},

/**
* @internal
*/
__getAndroidHome: () => process.env.ANDROID_HOME
};

module.exports = AndroidCommandLineTools;
105 changes: 105 additions & 0 deletions spec/unit/AndroidCommandLineTools.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
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.
*/

const fs = require('node:fs');
const path = require('node:path');
const AndroidCommandLineTools = require('../../lib/env/AndroidCommandLineTools');

describe('AndroidCommandLineTools', () => {
beforeAll(() => {
// For the purposes of these test, we will assume ANDROID_HOME is proper.
spyOn(fs, 'existsSync').and.returnValue(true);
});

describe('getAvailableVersions', () => {
describe('should return a list of command line versions', () => {
it('in descending order', () => {
spyOn(fs, 'readdirSync').and.returnValue([
'10.0',
'15.0',
'13'
]);

expect(AndroidCommandLineTools.getAvailableVersions()).toEqual([
'15.0',
'13',
'10.0'
]);
});

it('stable releases appear before prereleases', () => {
spyOn(fs, 'readdirSync').and.returnValue([
'15.0-rc01',
'15.0-alpha01',
'15.0'
]);

expect(AndroidCommandLineTools.getAvailableVersions()).toEqual([
'15.0',
'15.0-rc01',
'15.0-alpha01'
]);
});

it('"latest" should take all precedence', () => {
spyOn(fs, 'readdirSync').and.returnValue([
'15.0-rc01',
'15.0-alpha01',
'15.0',
'latest'
]);

expect(AndroidCommandLineTools.getAvailableVersions()).toEqual([
'latest',
'15.0',
'15.0-rc01',
'15.0-alpha01'
]);
});

it('invalid versions are ignored', () => {
spyOn(fs, 'readdirSync').and.returnValue([
'15.0-rc01',
'xyz',
'15.0'
]);

expect(AndroidCommandLineTools.getAvailableVersions()).toEqual([
'15.0',
'15.0-rc01'
]);
});
});
});

describe('getBinPath', () => {
beforeEach(() => {
spyOn(AndroidCommandLineTools, '__getAndroidHome').and.returnValue('/Android/Sdk');
});

it('should return the bin path of the latest version', () => {
spyOn(AndroidCommandLineTools, 'getAvailableVersions').and.returnValue([
'19.0',
'18.0'
]);

expect(AndroidCommandLineTools.getBinPath()).toBe(path.resolve('/Android/Sdk/cmdline-tools/19.0/bin'));
});
});
});
12 changes: 8 additions & 4 deletions spec/unit/check_reqs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const which = require('which');
const {
SDK_VERSION: DEFAULT_TARGET_API
} = require('../../lib/gradle-config-defaults');
const AndroidCommandLineTools = require('../../lib/env/AndroidCommandLineTools');

describe('check_reqs', function () {
let check_reqs;
Expand Down Expand Up @@ -58,6 +59,10 @@ describe('check_reqs', function () {
});

describe('check_android', function () {
beforeAll(() => {
spyOn(AndroidCommandLineTools, 'getAvailableVersions').and.returnValue(['latest']);
});

describe('find and set ANDROID_HOME when neither ANDROID_HOME nor ANDROID_SDK_ROOT is set', function () {
beforeEach(function () {
delete process.env.ANDROID_HOME;
Expand Down Expand Up @@ -123,13 +128,13 @@ describe('check_reqs', function () {
spyOn(fs, 'existsSync').and.returnValue(true);
spyOn(which, 'sync').and.callFake(function (cmd) {
if (cmd === 'avdmanager') {
return path.normalize('/android/sdk/tools/bin/avdmanager');
return path.resolve('/android/sdk/cmdline-tools/latest/bin/avdmanager');
} else {
return null;
}
});
return check_reqs.check_android().then(function () {
expect(process.env.ANDROID_HOME).toEqual(path.normalize('/android/sdk'));
expect(process.env.ANDROID_HOME).toEqual(path.resolve('/android/sdk'));
});
});
it('should error out if `avdmanager` command exists in a non-SDK-like directory structure', () => {
Expand Down Expand Up @@ -208,9 +213,8 @@ describe('check_reqs', function () {
});
it('should add tools/bin,tools,platform-tools to PATH if `avdmanager`,`android`,`adb` is not found', () => {
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');
expect(process.env.PATH).toContain('let the children play' + path.sep + 'cmdline-tools' + path.sep + 'latest' + path.sep + 'bin');
});
});
});
Expand Down
Loading