mirror of
https://github.com/vesper8/Quest2Frame.git
synced 2026-10-18 15:01:49 +02:00
80 lines
3.9 KiB
Python
80 lines
3.9 KiB
Python
# SPDX-License-Identifier: GPL-3.0-only
|
|
"""Build an opt-in experimental passthrough APK. Does not deploy or alter inputs."""
|
|
import argparse
|
|
from pathlib import Path
|
|
import re
|
|
import struct
|
|
import zipfile
|
|
|
|
from bridge import Bridge, ROOT, valid_package
|
|
|
|
|
|
def rename_soname(data, expected=b'libopenxr_loader.so', replacement=b'libq2f_original.so'):
|
|
"""Rename only DT_SONAME in an ELF64 LE library, without relocating sections."""
|
|
if data[:6] != b'\x7fELF\x02\x01' or len(replacement) > len(expected):
|
|
raise ValueError('Expected ELF64 little-endian and a non-growing SONAME')
|
|
data = bytearray(data)
|
|
offset = struct.unpack_from('<Q', data, 32)[0]
|
|
size, count = struct.unpack_from('<HH', data, 54)
|
|
segments = [struct.unpack_from('<IIQQQQQQ', data, offset+i*size) for i in range(count)]
|
|
dynamic = next(p for p in segments if p[0] == 2)
|
|
tags = {}
|
|
for pos in range(dynamic[2], dynamic[2]+dynamic[5], 16):
|
|
tag, value = struct.unpack_from('<QQ', data, pos)
|
|
if not tag:
|
|
break
|
|
tags[tag] = value
|
|
address = tags[5]+tags[14]
|
|
segment = next(p for p in segments if p[0] == 1 and p[3] <= address < p[3]+p[5])
|
|
start = segment[2]+address-segment[3]
|
|
end = data.index(0, start)
|
|
if data[start:end] != expected:
|
|
raise ValueError('Unexpected original SONAME; refusing to patch')
|
|
data[start:end+1] = replacement + b'\0'*(end-start+1-len(replacement))
|
|
return bytes(data)
|
|
|
|
|
|
def build(apk, output, bridge=None):
|
|
apk = Path(apk).resolve()
|
|
output = Path(output).resolve()
|
|
output.mkdir(parents=True, exist_ok=False)
|
|
bridge = bridge or Bridge()
|
|
java, _, tools, clang = bridge.toolchain()
|
|
if not clang:
|
|
raise RuntimeError('Android NDK is required')
|
|
badging = bridge.run([tools/'aapt.exe', 'dump', 'badging', apk])
|
|
package = valid_package(re.search(r"package: name='([^']+)'", badging)[1])
|
|
prefix = 'lib/arm64-v8a/'
|
|
with zipfile.ZipFile(apk) as archive:
|
|
if prefix+'libq2f_original.so' in archive.namelist():
|
|
raise ValueError('Input already contains the experimental adapter')
|
|
(output/'libq2f_original.so').write_bytes(rename_soname(archive.read(prefix+'libopenxr_loader.so')))
|
|
adapter = output/'libopenxr_loader.so'
|
|
bridge.run([clang, '--target=aarch64-linux-android29',
|
|
f'--sysroot={clang.parent.parent / "sysroot"}', '-shared', '-fPIC', '-O2',
|
|
'-Wall', '-Wextra', '-Werror', '-Wl,-Bsymbolic', '-Wl,-z,max-page-size=16384',
|
|
'-Wl,-soname,libopenxr_loader.so', '-I', ROOT/'native/include',
|
|
ROOT/'native/passthrough_bridge.c', '-Wl,--no-as-needed',
|
|
output/'libq2f_original.so', '-ldl', '-llog', '-o', adapter])
|
|
with zipfile.ZipFile(apk) as src, zipfile.ZipFile(output/'unsigned.apk', 'w', zipfile.ZIP_DEFLATED) as dst:
|
|
for item in src.infolist():
|
|
if item.filename.startswith('META-INF/') or item.filename == prefix+'libopenxr_loader.so':
|
|
continue
|
|
dst.writestr(item, src.read(item.filename))
|
|
dst.write(adapter, prefix+adapter.name)
|
|
dst.write(output/'libq2f_original.so', prefix+'libq2f_original.so')
|
|
bridge.run([tools/'zipalign.exe', '-f', '-P', '16', '4', output/'unsigned.apk', output/'aligned.apk'])
|
|
key = ROOT/'tools/ovrport-workspace/signatures'/f'{package}.keystore'
|
|
bridge.run([java, '-jar', tools/'lib/apksigner.jar', 'sign', '--ks', key,
|
|
'--ks-pass', 'pass:password', '--out', output/'game.apk', output/'aligned.apk'])
|
|
bridge.run([java, '-jar', tools/'lib/apksigner.jar', 'verify', '--verbose', output/'game.apk'])
|
|
print(f'Experimental APK: {output / "game.apk"}. Original APK unchanged.')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('apk', type=Path)
|
|
parser.add_argument('output', type=Path, help='New directory; must not exist')
|
|
args = parser.parse_args()
|
|
build(args.apk, args.output)
|