This is an automated email from the ASF dual-hosted git repository.
normanbreau pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-android.git
The following commit(s) were added to refs/heads/master by this push:
new 76bac55f fix: cordova requirements command and SDK lookup based on
tools (#1877)
76bac55f is described below
commit 76bac55fba688f3ad472a0fa20362c58287066da
Author: Norman Breau <[email protected]>
AuthorDate: Thu Dec 18 09:23:01 2025 -0400
fix: cordova requirements command and SDK lookup based on tools (#1877)
* fix: cordova requirements command and SDK lookup based on tools
* Update spec/unit/AndroidCommandLineTools.spec.js
Co-authored-by: エリス <[email protected]>
* Update lib/env/AndroidCommandLineTools.js
Co-authored-by: エリス <[email protected]>
* Update lib/env/AndroidCommandLineTools.js
Co-authored-by: エリス <[email protected]>
* Update lib/env/AndroidCommandLineTools.js
---------
Co-authored-by: エリス <[email protected]>
---
lib/check_reqs.js | 18 +++--
lib/env/AndroidCommandLineTools.js | 102 +++++++++++++++++++++++++++++
spec/unit/AndroidCommandLineTools.spec.js | 105 ++++++++++++++++++++++++++++++
spec/unit/check_reqs.spec.js | 12 ++--
4 files changed, 226 insertions(+), 11 deletions(-)
diff --git a/lib/check_reqs.js b/lib/check_reqs.js
index 6e1bfd4a..217f5dd6 100644
--- a/lib/check_reqs.js
+++ b/lib/check_reqs.js
@@ -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.
@@ -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');
}
}
}
@@ -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;
});
diff --git a/lib/env/AndroidCommandLineTools.js
b/lib/env/AndroidCommandLineTools.js
new file mode 100644
index 00000000..e99ccc3c
--- /dev/null
+++ b/lib/env/AndroidCommandLineTools.js
@@ -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;
diff --git a/spec/unit/AndroidCommandLineTools.spec.js
b/spec/unit/AndroidCommandLineTools.spec.js
new file mode 100644
index 00000000..dc7f1fab
--- /dev/null
+++ b/spec/unit/AndroidCommandLineTools.spec.js
@@ -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'));
+ });
+ });
+});
diff --git a/spec/unit/check_reqs.spec.js b/spec/unit/check_reqs.spec.js
index 54df1cf7..4628f41a 100644
--- a/spec/unit/check_reqs.spec.js
+++ b/spec/unit/check_reqs.spec.js
@@ -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;
@@ -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;
@@ -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', () => {
@@ -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');
});
});
});
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]