XAPK, APKM and APKS¶
Google Play builds most apps from App Bundles and installs them as a set of APKs: a base APK plus configuration splits for the CPU architecture, screen density and language. Third-party stores and backup tools package such sets into a single archive:
| Format | Made by | Marker | Base APK |
|---|---|---|---|
| XAPK | APKPure, APKCombo | manifest.json |
the base entry of split_apks, <package_name>.apk, or base.apk |
| APKM | APKMirror | info.json |
base.apk |
| APKS | bundletool build-apks |
toc.pb |
splits/base-master.apk, universal.apk, standalones/… |
| APKS | SAI, App Manager | meta.sai_v*.json, info.json |
base.apk |
Opening a container¶
APK(path) detects the format and parses the base APK.
The candidates are tried in order until one parses: the base named in manifest.json (the base entry of split_apks, then <package_name>.apk), base.apk, then the base of a bundletool APKS. A broken manifest.json or a broken root AndroidManifest.xml only produces a warning when a base APK is found. Containers over 4 GB (ZIP64) are supported.
When none of these names is present and the archive has no AndroidManifest.xml of its own, apk-info guesses: the same names in another case or directory (BASE.APK, app/base.apk), then any APK at the root. A guess is accepted only if it is not a split, and a warning names it. An APK with a broken manifest is never treated as a container, so an APK it carries (for example a dropper's payload) is not reported as the app.
For a bundletool APKS, get_min_sdk_version() reports the lowest SDK the variants in toc.pb target. For an app with a minimum below 21, bundletool raises minSdkVersion of the split APKs to 21, since split installs need Lollipop, and keeps the app's own minimum in the standalones/ APKs for older devices.
apk-info extract -r decodes the resources of the same base APK, plus its splits.
What comes from where:
| Data | Source |
|---|---|
| manifest getters, components, permissions, resources | base APK |
get_signatures(), is_multidex() |
base APK |
get_supported_abis() |
base APK and the names of config.<abi>.apk splits |
get_container_signatures() |
the outer archive (APKMirror signs APKM files, most others are unsigned) |
namelist(), read() |
the outer archive; the base APK with base=True |
Reading files of the base APK¶
By default namelist() and read() work on the outer archive, so they return manifest.json, toc.pb and the split APKs themselves, not classes.dex:
apk = APK("app.apks")
apk.namelist() # ['toc.pb', 'splits/base-master.apk', 'splits/base-ru.apk', ...]
Pass base=True to namelist() and read():
from apk_info import APK
apk = APK("org.telegram.messenger.xapk")
dex = [n for n in apk.namelist(base=True) if n.endswith(".dex")]
data, _ = apk.read("classes.dex", base=True)
For a plain APK, base=True changes nothing, so the same code works for both.
get_base_zip() returns the base APK of a container, or the APK itself:
extract unpacks the outer archive. Extract the base APK, then extract it again:
Resources of all splits¶
Language strings and density-specific drawables live in the config splits, not in the base APK. extract -r merges them:
apk-info extract org.telegram.messenger.xapk -r -o out/
ls out/org.telegram.messenger.xapk.unp/res/ | grep values-
Limitations¶
- A container nested in another archive (a ZIP holding an XAPK) is not opened.
- App Bundles (
.aab) are rejected with an explicit error: build APKS from them with bundletool. - Compressed (DEFLATED) entries over 4 GB can't be read or extracted; stored ones, which is how stores write APKs and OBB files, are fine.
- There is no API yet to list splits, their config qualifiers or their signatures, or the OBB files of an XAPK.
- Encrypted APKM files from before 2021 are rejected with a hint to decrypt them with unapkm.