appium-espresso-driver 8.5.5 → 8.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +4 -2
- package/espresso-server/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk +0 -0
- package/espresso-server/buildSrc/.gradle/9.5.1/executionHistory/executionHistory.bin +0 -0
- package/espresso-server/buildSrc/.gradle/9.5.1/executionHistory/executionHistory.lock +0 -0
- package/espresso-server/buildSrc/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/espresso-server/buildSrc/.gradle/buildOutputCleanup/cache.properties +1 -1
- package/espresso-server/buildSrc/.gradle/file-system.probe +0 -0
- package/espresso-server/gradle/libs.versions.toml +1 -1
- package/espresso-server/gradle.properties +1 -1
- package/espresso-server/library/src/main/java/io/appium/espressoserver/lib/helpers/Version.kt +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/scripts/build-espresso.mjs +6 -2
- package/scripts/diagnose-app.mjs +84 -0
- package/scripts/lib/dependency-versions/apk-dex.mjs +87 -0
- package/scripts/lib/dependency-versions/apk-kotlin-metadata.mjs +70 -0
- package/scripts/lib/dependency-versions/apk-meta-inf.mjs +70 -0
- package/scripts/lib/dependency-versions/apk-scan.mjs +35 -0
- package/scripts/lib/dependency-versions/comparison.mjs +213 -0
- package/scripts/lib/dependency-versions/gradle-scan.mjs +167 -0
- package/scripts/lib/dependency-versions/index.mjs +23 -0
- package/scripts/lib/dependency-versions/report.mjs +96 -0
- package/scripts/lib/dependency-versions/server-versions.mjs +23 -0
- package/scripts/lib/dependency-versions/tracked-modules.mjs +86 -0
- package/scripts/lib/dependency-versions/types.mjs +40 -0
- package/scripts/lib/dependency-versions/version-utils.mjs +96 -0
- package/scripts/lib/diagnose/apk-manifest.mjs +25 -0
- package/scripts/lib/diagnose/app-input.mjs +74 -0
- package/scripts/lib/diagnose/checks/androidx.mjs +40 -0
- package/scripts/lib/diagnose/checks/compile-sdk.mjs +52 -0
- package/scripts/lib/diagnose/checks/dependency.mjs +31 -0
- package/scripts/lib/diagnose/checks/index.mjs +8 -0
- package/scripts/lib/diagnose/checks/input-kind.mjs +21 -0
- package/scripts/lib/diagnose/checks/internet-permission.mjs +67 -0
- package/scripts/lib/diagnose/checks/lifecycle-extensions.mjs +23 -0
- package/scripts/lib/diagnose/checks/obfuscation.mjs +50 -0
- package/scripts/lib/diagnose/checks/startup-provider.mjs +40 -0
- package/scripts/lib/diagnose/diagnosis.mjs +54 -0
- package/scripts/lib/diagnose/espresso-build-config.mjs +38 -0
- package/scripts/lib/diagnose/gradle-utils.mjs +45 -0
- package/scripts/lib/diagnose/index.mjs +7 -0
- package/scripts/lib/diagnose/project-input.mjs +20 -0
- package/scripts/lib/diagnose/report.mjs +53 -0
- package/scripts/lib/diagnose/summary.mjs +21 -0
- package/scripts/lib/diagnose/types.mjs +49 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [8.6.0](https://github.com/appium/appium-espresso-driver/compare/v8.5.6...v8.6.0) (2026-05-24)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* Add a script for app diagnosis ([#1175](https://github.com/appium/appium-espresso-driver/issues/1175)) ([c2b48e8](https://github.com/appium/appium-espresso-driver/commit/c2b48e828d8595c90cba4e5222e21485f99b686d))
|
|
6
|
+
|
|
7
|
+
## [8.5.6](https://github.com/appium/appium-espresso-driver/compare/v8.5.5...v8.5.6) (2026-05-23)
|
|
8
|
+
|
|
9
|
+
### Miscellaneous Chores
|
|
10
|
+
|
|
11
|
+
* **deps:** bump composeUiTest from 1.1.1 to 1.11.2 in /espresso-server ([#1170](https://github.com/appium/appium-espresso-driver/issues/1170)) ([417a42a](https://github.com/appium/appium-espresso-driver/commit/417a42a05e1dedcef3c56babd0ca21d492da0b38))
|
|
12
|
+
|
|
1
13
|
## [8.5.5](https://github.com/appium/appium-espresso-driver/compare/v8.5.4...v8.5.5) (2026-05-23)
|
|
2
14
|
|
|
3
15
|
### Miscellaneous Chores
|
package/README.md
CHANGED
|
@@ -239,6 +239,8 @@ The `swiper` argument is not supported in Compose mode. Available since driver v
|
|
|
239
239
|
|
|
240
240
|
Calling other driver element-specific APIs not listed above would most likely throw an exception as Compose and Espresso elements are being stored in completely separated internal caches and must not be mixed.
|
|
241
241
|
|
|
242
|
+
If session startup or Compose interactions fail, see [How To Troubleshoot Jetpack Compose Apps](docs/compose-troubleshooting.md).
|
|
243
|
+
|
|
242
244
|
You could also check end-to-end tests for more examples on how to setup test capabilities and
|
|
243
245
|
on the Compose usage in general:
|
|
244
246
|
- https://github.com/appium/appium-espresso-driver/blob/master/test/functional/commands/jetpack-componse-element-values-e2e-specs.js
|
|
@@ -1830,6 +1832,8 @@ more details.
|
|
|
1830
1832
|
|
|
1831
1833
|
## Troubleshooting
|
|
1832
1834
|
|
|
1835
|
+
* Run `appium driver run espresso diagnose-app --app /path/to/your/android-project` or `--app /path/to/debug.apk`. The script checks manifest permissions, obfuscation, AndroidX/Compose dependency alignment, and other static preconditions for precompile. Exit code is non-zero when the app is not ready for precompile.
|
|
1836
|
+
* If you test **Jetpack Compose** apps (or hybrid View + Compose UIs) and see server startup failures, `NoSuchMethodError` in `androidx.compose.*`, misleading `INTERNET` permission errors, `InitializationProvider` / `Resources$NotFoundException`, or locator issues in `compose` subdriver mode, read [How To Troubleshoot Jetpack Compose Apps](docs/compose-troubleshooting.md).
|
|
1833
1837
|
* If you observe Espresso server crash on startup and various exceptions about missing class/method in the logcat output then consider updating [appium:espressoBuildConfig](#espresso-build-config) capability with module versions that match your application under test. This might require some experimentation, as different apps have different module requirements. Check, for example, [issue #812](https://github.com/appium/appium-espresso-driver/issues/812). Another solution might be
|
|
1834
1838
|
to [integrate](#consuming-espresso-server-as-library) Espresso Server with the application under test in form of a library.
|
|
1835
1839
|
* If you experience issues with application activities being not found or not starting then consider checking [How To Troubleshoot Activities Startup](docs/activity-startup.md) article.
|
|
@@ -1851,8 +1855,6 @@ to [integrate](#consuming-espresso-server-as-library) Espresso Server with the a
|
|
|
1851
1855
|
-keep class android.support.v7.** { *; }
|
|
1852
1856
|
```
|
|
1853
1857
|
Please read [#449](https://github.com/appium/appium-espresso-driver/issues/449#issuecomment-537833139) for more details on this topic.
|
|
1854
|
-
* When you want to build without compose dependencies
|
|
1855
|
-
* Espresso driver has Jetpack Compose dependencies to [support Jetpack Compose](#jetpack-compose-support). It could break the application under test's dependencies. The typical case is when the application under test does not have the Jetpack Compose dependencies. Then, you can try out [no compose dependencies branch](https://github.com/appium/appium-espresso-driver/pull/879)). In Appium 2.0, the branch is available as `appium driver install --source=local /path/to/the/appium-espress-driver` with the `no-compose-deps` branch instead of npm installation.
|
|
1856
1858
|
|
|
1857
1859
|
|
|
1858
1860
|
## Contributing
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#
|
|
1
|
+
#Sun May 24 16:52:48 UTC 2026
|
|
2
2
|
gradle.version=9.5.1
|
|
Binary file
|
package/espresso-server/library/src/main/java/io/appium/espressoserver/lib/helpers/Version.kt
CHANGED
|
@@ -2,6 +2,6 @@ package io.appium.espressoserver.lib.helpers
|
|
|
2
2
|
|
|
3
3
|
// This value is updated automatically by the NPM versioning script
|
|
4
4
|
// It should be in sync with the NPM module version from package.json
|
|
5
|
-
private const val VERSION = "8.
|
|
5
|
+
private const val VERSION = "8.6.0"
|
|
6
6
|
|
|
7
7
|
fun getEspressoServerVersion() = VERSION
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appium-espresso-driver",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.6.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "appium-espresso-driver",
|
|
9
|
-
"version": "8.
|
|
9
|
+
"version": "8.6.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"appium-adb": "^15.0.0",
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"automated testing",
|
|
8
8
|
"android"
|
|
9
9
|
],
|
|
10
|
-
"version": "8.
|
|
10
|
+
"version": "8.6.0",
|
|
11
11
|
"author": "Appium Contributors",
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"repository": {
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"mainClass": "EspressoDriver",
|
|
36
36
|
"scripts": {
|
|
37
37
|
"print-espresso-path": "./scripts/print-espresso-path.mjs",
|
|
38
|
-
"build-espresso": "./scripts/build-espresso.mjs"
|
|
38
|
+
"build-espresso": "./scripts/build-espresso.mjs",
|
|
39
|
+
"diagnose-app": "./scripts/diagnose-app.mjs"
|
|
39
40
|
},
|
|
40
41
|
"doctor": {
|
|
41
42
|
"checks": [
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {fileURLToPath} from 'node:url';
|
|
2
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
3
3
|
import {Command} from 'commander';
|
|
4
4
|
import {logger, fs} from 'appium/support.js';
|
|
5
|
-
import {ServerBuilder} from '../build/lib/commands/server/index.js';
|
|
6
5
|
|
|
7
6
|
const LOG = logger.getLogger('EspressoBuild');
|
|
8
7
|
|
|
9
8
|
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const SERVER_BUILDER_MODULE = pathToFileURL(
|
|
10
|
+
path.join(ROOT_DIR, 'build/lib/commands/server/index.js'),
|
|
11
|
+
).href;
|
|
10
12
|
const ESPRESSO_SERVER_ROOT = path.join(ROOT_DIR, 'espresso-server');
|
|
11
13
|
const ESPRESSO_SERVER_BUILD = path.join(ESPRESSO_SERVER_ROOT, 'app', 'build');
|
|
12
14
|
|
|
@@ -16,6 +18,7 @@ const ESPRESSO_SERVER_BUILD = path.join(ESPRESSO_SERVER_ROOT, 'app', 'build');
|
|
|
16
18
|
async function buildEspressoServer(options) {
|
|
17
19
|
LOG.info(`Building espresso server in '${ESPRESSO_SERVER_BUILD}'`);
|
|
18
20
|
|
|
21
|
+
/** @type {import('../lib/commands/server/builder.js').ServerBuilderOptions} */
|
|
19
22
|
const opts = {
|
|
20
23
|
serverPath: ESPRESSO_SERVER_ROOT,
|
|
21
24
|
showGradleLog: options.showGradleLog,
|
|
@@ -43,6 +46,7 @@ async function buildEspressoServer(options) {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
const {ServerBuilder} = await import(SERVER_BUILDER_MODULE);
|
|
46
50
|
const builder = new ServerBuilder(LOG, opts);
|
|
47
51
|
try {
|
|
48
52
|
await builder.build();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import {access} from 'node:fs/promises';
|
|
4
|
+
import {constants as fsConstants} from 'node:fs';
|
|
5
|
+
import {Command} from 'commander';
|
|
6
|
+
import {logger} from 'appium/support.js';
|
|
7
|
+
import {
|
|
8
|
+
collectAppInputFromApk,
|
|
9
|
+
collectAppInputFromProject,
|
|
10
|
+
formatDiagnosisReport,
|
|
11
|
+
loadEspressoServerDefaults,
|
|
12
|
+
runDiagnosis,
|
|
13
|
+
} from './lib/diagnose/index.mjs';
|
|
14
|
+
|
|
15
|
+
const LOG = logger.getLogger('EspressoDiagnose');
|
|
16
|
+
|
|
17
|
+
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const ESPRESSO_SERVER_ROOT = path.join(ROOT_DIR, 'espresso-server');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} targetPath
|
|
22
|
+
*/
|
|
23
|
+
async function pathExists(targetPath) {
|
|
24
|
+
try {
|
|
25
|
+
await access(targetPath, fsConstants.F_OK);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} inputPath
|
|
34
|
+
*/
|
|
35
|
+
async function resolveAppInput(inputPath) {
|
|
36
|
+
const resolved = path.resolve(inputPath);
|
|
37
|
+
if (!(await pathExists(resolved))) {
|
|
38
|
+
throw new Error(`Path does not exist: ${resolved}`);
|
|
39
|
+
}
|
|
40
|
+
if (resolved.endsWith('.apk')) {
|
|
41
|
+
return collectAppInputFromApk(resolved);
|
|
42
|
+
}
|
|
43
|
+
if (resolved.endsWith('.aab')) {
|
|
44
|
+
throw new Error('Only .apk files are supported for binary analysis. Use the Gradle project root for .aab.');
|
|
45
|
+
}
|
|
46
|
+
return collectAppInputFromProject(resolved);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function main() {
|
|
50
|
+
const program = new Command();
|
|
51
|
+
program
|
|
52
|
+
.name('appium driver run espresso diagnose-app')
|
|
53
|
+
.description(
|
|
54
|
+
'Diagnose whether an Android app is ready to embed a precompiled Espresso server',
|
|
55
|
+
)
|
|
56
|
+
.requiredOption(
|
|
57
|
+
'--app <path>',
|
|
58
|
+
'Gradle project root of the AUT, or path to a built .apk (debug APK recommended)',
|
|
59
|
+
)
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
const [appInput, serverDefaults] = await Promise.all([
|
|
62
|
+
resolveAppInput(options.app),
|
|
63
|
+
loadEspressoServerDefaults(ESPRESSO_SERVER_ROOT, ROOT_DIR),
|
|
64
|
+
]);
|
|
65
|
+
const report = await runDiagnosis(appInput, serverDefaults);
|
|
66
|
+
|
|
67
|
+
LOG.info(`App: ${appInput.kind} at ${appInput.path}`);
|
|
68
|
+
LOG.info(`Driver: appium-espresso-driver@${serverDefaults.driverVersion}`);
|
|
69
|
+
if (appInput.sources?.length) {
|
|
70
|
+
LOG.info(
|
|
71
|
+
`Scanned: ${appInput.sources.slice(0, 8).join(', ')}${appInput.sources.length > 8 ? '…' : ''}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
LOG.info('');
|
|
75
|
+
LOG.info(formatDiagnosisReport(report));
|
|
76
|
+
if (!report.ready) {
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await program.parseAsync(process.argv);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await main();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {fs, zip} from 'appium/support.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} apkPath
|
|
6
|
+
* @param {string} destDir
|
|
7
|
+
*/
|
|
8
|
+
export async function extractApk(apkPath, destDir) {
|
|
9
|
+
try {
|
|
10
|
+
await zip.extractAllTo(apkPath, destDir);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
throw new Error(`Failed to extract APK.`, {cause: err});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} extractRoot
|
|
18
|
+
* @returns {Promise<string>}
|
|
19
|
+
*/
|
|
20
|
+
export async function readAllDexStrings(extractRoot) {
|
|
21
|
+
/** @type {string[]} */
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = await fs.readdir(extractRoot);
|
|
26
|
+
} catch {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
for (const name of entries) {
|
|
30
|
+
if (!/^classes\d*\.dex$/i.test(name)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const buf = await fs.readFile(path.join(extractRoot, name));
|
|
35
|
+
chunks.push(buf.toString('latin1'));
|
|
36
|
+
} catch {
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return chunks.join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} dexStrings
|
|
45
|
+
* @param {string} corpus
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function detectProguardLikely(dexStrings, corpus) {
|
|
49
|
+
const androidxHits = (corpus.match(/androidx\//g) ?? []).length;
|
|
50
|
+
const hasR8 = /\bR8\b/.test(corpus) || /proguard/i.test(corpus);
|
|
51
|
+
const fewReadableAndroidx = androidxHits < 3 && dexStrings.length > 100_000;
|
|
52
|
+
const obfuscatedKotlin = (dexStrings.match(/\bL[a-z]{1,2}\/[a-z]{1,2};/g) ?? []).length > 500;
|
|
53
|
+
return (hasR8 && fewReadableAndroidx) || (obfuscatedKotlin && fewReadableAndroidx);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} root
|
|
58
|
+
* @returns {Promise<string>}
|
|
59
|
+
*/
|
|
60
|
+
export async function collectMetaInfStrings(root) {
|
|
61
|
+
const metaDir = path.join(root, 'META-INF');
|
|
62
|
+
/** @type {string[]} */
|
|
63
|
+
const chunks = [];
|
|
64
|
+
/** @param {string} dir */
|
|
65
|
+
async function walkMeta(dir) {
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = await fs.readdir(dir, {withFileTypes: true});
|
|
69
|
+
} catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const full = path.join(dir, entry.name);
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
await walkMeta(full);
|
|
76
|
+
} else if (/\.(properties|version|kotlin_module|txt)$/i.test(entry.name)) {
|
|
77
|
+
try {
|
|
78
|
+
chunks.push(await fs.readFile(full, 'utf8'));
|
|
79
|
+
} catch {
|
|
80
|
+
// ignore binary
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await walkMeta(metaDir);
|
|
86
|
+
return chunks.join('\n');
|
|
87
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {fs} from 'appium/support.js';
|
|
2
|
+
import {ADB} from 'appium-adb';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {exec} from 'teen_process';
|
|
5
|
+
import {normalizeVersion} from './version-utils.mjs';
|
|
6
|
+
|
|
7
|
+
/** @type {Promise<import('appium-adb').ADB> | undefined} */
|
|
8
|
+
let sdkAdbPromise;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} dexdumpOutput
|
|
12
|
+
* @returns {string[]}
|
|
13
|
+
*/
|
|
14
|
+
export function parseKotlinMetadataVersionsFromDexdump(dexdumpOutput) {
|
|
15
|
+
/** @type {Set<string>} */
|
|
16
|
+
const versions = new Set();
|
|
17
|
+
const mvPattern = /mv=\{\s*(\d+)\s+(\d+)\s+(\d+)\s*\}/g;
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = mvPattern.exec(dexdumpOutput)) !== null) {
|
|
20
|
+
const version = normalizeVersion(`${match[1]}.${match[2]}.${match[3]}`);
|
|
21
|
+
if (version) {
|
|
22
|
+
versions.add(version);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [...versions];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} extractRoot APK extract directory
|
|
30
|
+
* @param {Record<string, Set<string>>} found Module id → version set (mutated)
|
|
31
|
+
*/
|
|
32
|
+
export async function mergeKotlinMetadataVersionsFromDex(extractRoot, found) {
|
|
33
|
+
if (!found.kotlin) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let dexdump;
|
|
37
|
+
try {
|
|
38
|
+
const adb = await getSdkAdb();
|
|
39
|
+
dexdump = await adb.getSdkBinaryPath('dexdump');
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = await fs.readdir(extractRoot);
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const name of entries) {
|
|
50
|
+
if (!/^classes\d*\.dex$/i.test(name)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const {stdout} = await exec(dexdump, ['-a', path.join(extractRoot, name)]);
|
|
55
|
+
for (const version of parseKotlinMetadataVersionsFromDexdump(stdout)) {
|
|
56
|
+
found.kotlin.add(version);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// try next dex file
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @returns {Promise<import('appium-adb').ADB>} */
|
|
65
|
+
async function getSdkAdb() {
|
|
66
|
+
if (!sdkAdbPromise) {
|
|
67
|
+
sdkAdbPromise = ADB.createADB({suppressKillServer: true});
|
|
68
|
+
}
|
|
69
|
+
return sdkAdbPromise;
|
|
70
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {fs} from 'appium/support.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {normalizeVersion} from './version-utils.mjs';
|
|
4
|
+
|
|
5
|
+
/** @type {ReadonlyArray<{moduleId: string, matches: (base: string) => boolean}>} */
|
|
6
|
+
const META_INF_VERSION_MODULE_RULES = [
|
|
7
|
+
{moduleId: 'compose', matches: (base) => base.startsWith('androidx.compose.')},
|
|
8
|
+
{moduleId: 'kotlin', matches: (base) => base.startsWith('org.jetbrains.kotlin_')},
|
|
9
|
+
{moduleId: 'espresso', matches: (base) => base.startsWith('androidx.test.espresso')},
|
|
10
|
+
{
|
|
11
|
+
moduleId: 'annotation',
|
|
12
|
+
matches: (base) =>
|
|
13
|
+
base.startsWith('androidx.annotation_annotation') && !base.includes('experimental'),
|
|
14
|
+
},
|
|
15
|
+
{moduleId: 'uiautomator', matches: (base) => base.startsWith('androidx.test.uiautomator')},
|
|
16
|
+
{
|
|
17
|
+
moduleId: 'androidxTest',
|
|
18
|
+
matches: (base) =>
|
|
19
|
+
base.startsWith('androidx.test.') &&
|
|
20
|
+
!base.startsWith('androidx.test.espresso') &&
|
|
21
|
+
!base.startsWith('androidx.test.uiautomator'),
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} base Basename without `.version` (e.g. `androidx.compose.ui_ui`)
|
|
27
|
+
* @returns {string | null}
|
|
28
|
+
*/
|
|
29
|
+
export function mapMetaInfVersionBaseToModule(base) {
|
|
30
|
+
for (const {moduleId, matches} of META_INF_VERSION_MODULE_RULES) {
|
|
31
|
+
if (matches(base)) {
|
|
32
|
+
return moduleId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} extractRoot APK extract directory
|
|
40
|
+
* @param {Record<string, Set<string>>} found Module id → version set (mutated)
|
|
41
|
+
*/
|
|
42
|
+
export async function mergeMetaInfEmbeddedVersions(extractRoot, found) {
|
|
43
|
+
const metaDir = path.join(extractRoot, 'META-INF');
|
|
44
|
+
let versionFiles;
|
|
45
|
+
try {
|
|
46
|
+
versionFiles = await fs.glob('**/*.version', {cwd: metaDir, absolute: true});
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const filePath of versionFiles) {
|
|
51
|
+
const base = path.basename(filePath, '.version');
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = (await fs.readFile(filePath, 'utf8')).trim();
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!raw || /writeVersionFile|^\s*task\s*:/i.test(raw)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const version = normalizeVersion(raw);
|
|
62
|
+
if (!version) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const moduleId = mapMetaInfVersionBaseToModule(base);
|
|
66
|
+
if (moduleId && found[moduleId]) {
|
|
67
|
+
found[moduleId].add(version);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {fs, tempDir} from 'appium/support.js';
|
|
2
|
+
import {createEmptyVersionSets} from './tracked-modules.mjs';
|
|
3
|
+
import {collectVersionsFromCorpus, versionSetsToSortedRecords} from './version-utils.mjs';
|
|
4
|
+
import {extractApk, readAllDexStrings, detectProguardLikely, collectMetaInfStrings} from './apk-dex.mjs';
|
|
5
|
+
import {mergeMetaInfEmbeddedVersions} from './apk-meta-inf.mjs';
|
|
6
|
+
import {mergeKotlinMetadataVersionsFromDex} from './apk-kotlin-metadata.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} apkPath
|
|
10
|
+
* @returns {Promise<{versions: Record<string, string[]>, proguardLikely: boolean, sources: string[]}>}
|
|
11
|
+
*/
|
|
12
|
+
export async function collectAppVersionsFromApk(apkPath) {
|
|
13
|
+
const extractDir = await tempDir.openDir();
|
|
14
|
+
try {
|
|
15
|
+
await extractApk(apkPath, extractDir);
|
|
16
|
+
const dexStrings = await readAllDexStrings(extractDir);
|
|
17
|
+
const metaStrings = await collectMetaInfStrings(extractDir);
|
|
18
|
+
const found = createEmptyVersionSets();
|
|
19
|
+
const corpus = `${dexStrings}\n${metaStrings}`;
|
|
20
|
+
|
|
21
|
+
collectVersionsFromCorpus(corpus, found);
|
|
22
|
+
await mergeMetaInfEmbeddedVersions(extractDir, found);
|
|
23
|
+
await mergeKotlinMetadataVersionsFromDex(extractDir, found);
|
|
24
|
+
|
|
25
|
+
const proguardLikely = detectProguardLikely(dexStrings, corpus);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
versions: versionSetsToSortedRecords(found),
|
|
29
|
+
proguardLikely,
|
|
30
|
+
sources: ['APK DEX/META-INF scan (incl. embedded *.version metadata)'],
|
|
31
|
+
};
|
|
32
|
+
} finally {
|
|
33
|
+
await fs.rimraf(extractDir);
|
|
34
|
+
}
|
|
35
|
+
}
|