214 lines
8.5 KiB
Python
214 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Packs an APK without aapt: binary AndroidManifest.xml, a tiny resources.arsc
|
|
(icon + app name), classes.dex. Output is unsigned; build.sh signs and aligns it.
|
|
|
|
usage: mkapk.py AndroidManifest.xml classes.dex icon.png out.apk
|
|
"""
|
|
import struct
|
|
import sys
|
|
import zipfile
|
|
import xml.etree.ElementTree as ET
|
|
|
|
ANDROID_NS = "http://schemas.android.com/apk/res/android"
|
|
|
|
# android.R.attr ids (stable since API 1-4)
|
|
ATTR_IDS = {
|
|
"theme": 0x01010000, "label": 0x01010001, "icon": 0x01010002, "name": 0x01010003,
|
|
"permission": 0x01010006, "enabled": 0x0101000e, "exported": 0x01010010,
|
|
"configChanges": 0x0101001f, "minSdkVersion": 0x0101020c, "versionCode": 0x0101021b,
|
|
"versionName": 0x0101021c, "windowSoftInputMode": 0x0101022b,
|
|
"targetSdkVersion": 0x01010270, "installLocation": 0x010102b7,
|
|
}
|
|
INT_ATTRS = {"versionCode", "minSdkVersion", "targetSdkVersion"}
|
|
BOOL_ATTRS = {"exported", "enabled"}
|
|
FLAGS = {
|
|
"configChanges": {"mcc": 1, "mnc": 2, "locale": 4, "touchscreen": 8, "keyboard": 0x10,
|
|
"keyboardHidden": 0x20, "navigation": 0x40, "orientation": 0x80,
|
|
"screenLayout": 0x100, "uiMode": 0x200, "fontScale": 0x40000000},
|
|
"windowSoftInputMode": {"stateUnspecified": 0, "stateHidden": 2, "stateAlwaysHidden": 3,
|
|
"adjustResize": 0x10, "adjustPan": 0x20},
|
|
"installLocation": {"auto": 0, "internalOnly": 1, "preferExternal": 2},
|
|
}
|
|
|
|
T_REF, T_STRING, T_INT_DEC, T_INT_HEX, T_BOOL = 0x01, 0x03, 0x10, 0x11, 0x12
|
|
|
|
# Our own resources: type id -> (type name, [(key, value)])
|
|
RES_TYPES = [("drawable", [("icon", "res/drawable/icon.png")]),
|
|
("string", [("app_name", "GingerCal")])]
|
|
|
|
|
|
def res_id(ref):
|
|
typ, key = ref.lstrip("@").split("/")
|
|
for ti, (tname, entries) in enumerate(RES_TYPES):
|
|
for ei, (k, _) in enumerate(entries):
|
|
if tname == typ and k == key:
|
|
return 0x7F000000 | ((ti + 1) << 16) | ei
|
|
raise SystemExit("unknown resource " + ref)
|
|
|
|
|
|
# ------------------------------------------------------------- string pool
|
|
|
|
def string_pool(strings):
|
|
"""UTF-16 ResStringPool (understood by every Android version)."""
|
|
offsets, data = [], b""
|
|
for s in strings:
|
|
offsets.append(len(data))
|
|
u = s.encode("utf-16-le")
|
|
n = len(u) // 2
|
|
if n > 0x7FFF:
|
|
raise SystemExit("string too long")
|
|
data += struct.pack("<H", n) + u + b"\0\0"
|
|
while len(data) % 4:
|
|
data += b"\0"
|
|
header = 28
|
|
strings_start = header + 4 * len(strings)
|
|
size = strings_start + len(data)
|
|
return (struct.pack("<HHIIIIII", 0x0001, header, size, len(strings), 0, 0, strings_start, 0)
|
|
+ b"".join(struct.pack("<I", o) for o in offsets) + data)
|
|
|
|
|
|
# ---------------------------------------------------------------- AXML
|
|
|
|
def axml(manifest_path):
|
|
tree = ET.parse(manifest_path)
|
|
root = tree.getroot()
|
|
|
|
# collect strings: attribute names with resource ids first (resource map order)
|
|
attr_names, other = [], []
|
|
|
|
def walk(e):
|
|
for k in e.attrib:
|
|
ns, local = split(k)
|
|
if ns == ANDROID_NS:
|
|
if local not in ATTR_IDS:
|
|
raise SystemExit("add resource id for android:" + local)
|
|
if local not in attr_names:
|
|
attr_names.append(local)
|
|
for c in e:
|
|
walk(c)
|
|
|
|
def split(k):
|
|
if k.startswith("{"):
|
|
ns, local = k[1:].split("}")
|
|
return ns, local
|
|
return None, k
|
|
|
|
walk(root)
|
|
attr_names.sort(key=lambda n: ATTR_IDS[n])
|
|
strings = list(attr_names)
|
|
|
|
def idx(s):
|
|
if s not in strings:
|
|
strings.append(s)
|
|
return strings.index(s)
|
|
|
|
idx("android")
|
|
idx(ANDROID_NS)
|
|
|
|
events = [] # built after strings are known; strings may grow while encoding
|
|
|
|
def encode_value(local, ns, v):
|
|
if v.startswith("@") and "/" in v and not v.startswith("@android:"):
|
|
return 0xFFFFFFFF, T_REF, res_id(v)
|
|
if ns == ANDROID_NS and local in INT_ATTRS:
|
|
return 0xFFFFFFFF, T_INT_DEC, int(v)
|
|
if ns == ANDROID_NS and local in BOOL_ATTRS:
|
|
return 0xFFFFFFFF, T_BOOL, 0xFFFFFFFF if v == "true" else 0
|
|
if ns == ANDROID_NS and local in FLAGS:
|
|
n = 0
|
|
for part in v.split("|"):
|
|
n |= FLAGS[local][part.strip()]
|
|
return 0xFFFFFFFF, T_INT_HEX, n
|
|
si = idx(v)
|
|
return si, T_STRING, si
|
|
|
|
def element(e, line):
|
|
ns, local = split(e.tag)
|
|
attrs = []
|
|
for k, v in e.attrib.items():
|
|
ans, alocal = split(k)
|
|
raw, typ, data = encode_value(alocal, ans, v)
|
|
attrs.append((ATTR_IDS.get(alocal, 0) if ans == ANDROID_NS else 0,
|
|
idx(ANDROID_NS) if ans == ANDROID_NS else 0xFFFFFFFF, idx(alocal), raw, typ, data))
|
|
attrs.sort(key=lambda a: a[0]) # Android looks attributes up in resource-id order
|
|
name = idx(local)
|
|
body = struct.pack("<IIHHHHHH", 0xFFFFFFFF, name, 20, 20, len(attrs), 0, 0, 0)
|
|
for _, ans, an, raw, typ, data in attrs:
|
|
body += struct.pack("<IIIHBBI", ans, an, raw, 8, 0, typ, data & 0xFFFFFFFF)
|
|
events.append(struct.pack("<HHIII", 0x0102, 16, 16 + len(body), line, 0xFFFFFFFF) + body)
|
|
for c in e:
|
|
line = element(c, line + 1)
|
|
events.append(struct.pack("<HHIIIII", 0x0103, 16, 24, line, 0xFFFFFFFF, 0xFFFFFFFF, name))
|
|
return line + 1
|
|
|
|
ns_chunk = lambda t: struct.pack("<HHIIIII", t, 16, 24, 1, 0xFFFFFFFF, idx("android"), idx(ANDROID_NS))
|
|
events.append(ns_chunk(0x0100))
|
|
element(root, 2)
|
|
events.append(ns_chunk(0x0101))
|
|
|
|
pool = string_pool(strings)
|
|
resmap = struct.pack("<HHI", 0x0180, 8, 8 + 4 * len(attr_names)) + b"".join(
|
|
struct.pack("<I", ATTR_IDS[n]) for n in attr_names)
|
|
body = pool + resmap + b"".join(events)
|
|
return struct.pack("<HHI", 0x0003, 8, 8 + len(body)) + body
|
|
|
|
|
|
# ---------------------------------------------------------------- ARSC
|
|
|
|
def arsc(package):
|
|
values = [v for _, entries in RES_TYPES for _, v in entries]
|
|
global_pool = string_pool(values)
|
|
type_pool = string_pool([t for t, _ in RES_TYPES])
|
|
key_pool = string_pool([k for _, entries in RES_TYPES for k, _ in entries])
|
|
|
|
chunks = b""
|
|
vi = 0
|
|
ki = 0
|
|
for ti, (tname, entries) in enumerate(RES_TYPES):
|
|
tid = ti + 1
|
|
n = len(entries)
|
|
spec = struct.pack("<HHIBBHI", 0x0202, 16, 16 + 4 * n, tid, 0, 0, n) + b"\0\0\0\0" * n
|
|
config = struct.pack("<I", 32) + b"\0" * 28 # default configuration
|
|
header = 20 + len(config)
|
|
entry_data = b""
|
|
offsets = b""
|
|
for _ in entries:
|
|
offsets += struct.pack("<I", len(entry_data))
|
|
entry_data += struct.pack("<HHI", 8, 0, ki) + struct.pack("<HBBI", 8, 0, T_STRING, vi)
|
|
ki += 1
|
|
vi += 1
|
|
entries_start = header + 4 * n
|
|
typ = struct.pack("<HHIBBHII", 0x0201, header, entries_start + len(entry_data), tid, 0, 0, n,
|
|
entries_start) + config + offsets + entry_data
|
|
chunks += spec + typ
|
|
|
|
pkg_header = 284 # pre-Lollipop ResTable_package (no typeIdOffset)
|
|
name = package.encode("utf-16-le")[:254].ljust(256, b"\0")
|
|
type_off = pkg_header
|
|
key_off = type_off + len(type_pool)
|
|
pkg_size = key_off + len(key_pool) + len(chunks)
|
|
pkg = struct.pack("<HHII", 0x0200, pkg_header, pkg_size, 0x7F) + name + struct.pack(
|
|
"<IIII", type_off, len(RES_TYPES), key_off, ki) + type_pool + key_pool + chunks
|
|
assert len(struct.pack("<HHII", 0, 0, 0, 0) + name + struct.pack("<IIII", 0, 0, 0, 0)) == pkg_header
|
|
body = global_pool + pkg
|
|
return struct.pack("<HHII", 0x0002, 12, 12 + len(body), 1) + body
|
|
|
|
|
|
def main():
|
|
manifest, dex, icon, out = sys.argv[1:5]
|
|
package = ET.parse(manifest).getroot().attrib["package"]
|
|
with zipfile.ZipFile(out, "w") as z:
|
|
z.writestr(zipfile.ZipInfo("AndroidManifest.xml", (2026, 1, 1, 0, 0, 0)), axml(manifest),
|
|
compress_type=zipfile.ZIP_DEFLATED)
|
|
z.writestr(zipfile.ZipInfo("classes.dex", (2026, 1, 1, 0, 0, 0)), open(dex, "rb").read(),
|
|
compress_type=zipfile.ZIP_DEFLATED)
|
|
z.writestr(zipfile.ZipInfo("resources.arsc", (2026, 1, 1, 0, 0, 0)), arsc(package),
|
|
compress_type=zipfile.ZIP_STORED)
|
|
z.writestr(zipfile.ZipInfo("res/drawable/icon.png", (2026, 1, 1, 0, 0, 0)), open(icon, "rb").read(),
|
|
compress_type=zipfile.ZIP_STORED)
|
|
print("wrote", out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|