Files
gitlab-instance-0a899031_re…/run_redroid.py
2026-04-04 16:02:19 -05:00

432 lines
14 KiB
Python

#!/usr/bin/env python3
import argparse
import os
import re
import shlex
import socket
import subprocess
import sys
import time
import tools.compat as compat
from tools.helper import bcolors, print_color
COMMON_BRIDGE_PROPS = [
"ro.product.cpu.abilist=x86_64,arm64-v8a,x86,armeabi-v7a,armeabi",
"ro.product.cpu.abilist64=x86_64,arm64-v8a",
"ro.product.cpu.abilist32=x86,armeabi-v7a,armeabi",
"ro.dalvik.vm.isa.arm=x86",
"ro.dalvik.vm.isa.arm64=x86_64",
"ro.enable.native.bridge.exec=1",
"ro.enable.native.bridge.exec64=1",
"ro.vendor.enable.native.bridge.exec=1",
"ro.vendor.enable.native.bridge.exec64=1",
]
DEVICE_PROFILES = {
"pixel-3-xl": [
"ro.product.model=Pixel 3 XL",
"ro.product.brand=google",
"ro.product.name=crosshatch",
"ro.product.device=crosshatch",
"ro.product.manufacturer=Google",
"ro.product.system.model=Pixel 3 XL",
"ro.product.system.brand=google",
"ro.product.system.name=crosshatch",
"ro.product.system.device=crosshatch",
"ro.product.system.manufacturer=Google",
"ro.product.vendor.model=Pixel 3 XL",
"ro.product.vendor.brand=google",
"ro.product.vendor.name=crosshatch",
"ro.product.vendor.device=crosshatch",
"ro.product.vendor.manufacturer=Google",
"ro.product.product.model=Pixel 3 XL",
"ro.product.product.brand=google",
"ro.product.product.name=crosshatch",
"ro.product.product.device=crosshatch",
"ro.product.product.manufacturer=Google",
"ro.build.fingerprint=google/crosshatch/crosshatch:11/RQ3A.211001.001/7641976:user/release-keys",
"ro.system.build.fingerprint=google/crosshatch/crosshatch:11/RQ3A.211001.001/7641976:user/release-keys",
"ro.vendor.build.fingerprint=google/crosshatch/crosshatch:11/RQ3A.211001.001/7641976:user/release-keys",
"ro.product.build.fingerprint=google/crosshatch/crosshatch:11/RQ3A.211001.001/7641976:user/release-keys",
"ro.build.type=user",
"ro.build.tags=release-keys",
"ro.secure=1",
"ro.debuggable=0",
"ro.boot.verifiedbootstate=green",
"ro.boot.vbmeta.device_state=locked",
"ro.boot.flash.locked=1",
"ro.boot.veritymode=enforcing",
],
"pixel-7-pro": [
"ro.product.model=Pixel 7 Pro",
"ro.product.brand=google",
"ro.product.name=cheetah",
"ro.product.device=cheetah",
"ro.product.manufacturer=Google",
"ro.product.system.model=Pixel 7 Pro",
"ro.product.system.brand=google",
"ro.product.system.name=cheetah",
"ro.product.system.device=cheetah",
"ro.product.system.manufacturer=Google",
"ro.product.vendor.model=Pixel 7 Pro",
"ro.product.vendor.brand=google",
"ro.product.vendor.name=cheetah",
"ro.product.vendor.device=cheetah",
"ro.product.vendor.manufacturer=Google",
"ro.product.product.model=Pixel 7 Pro",
"ro.product.product.brand=google",
"ro.product.product.name=cheetah",
"ro.product.product.device=cheetah",
"ro.product.product.manufacturer=Google",
"ro.build.fingerprint=google/cheetah/cheetah:13/TQ3A.230805.001/2023080800:user/release-keys",
"ro.system.build.fingerprint=google/cheetah/cheetah:13/TQ3A.230805.001/2023080800:user/release-keys",
"ro.vendor.build.fingerprint=google/cheetah/cheetah:13/TQ3A.230805.001/2023080800:user/release-keys",
"ro.product.build.fingerprint=google/cheetah/cheetah:13/TQ3A.230805.001/2023080800:user/release-keys",
"ro.build.type=user",
"ro.build.tags=release-keys",
"ro.secure=1",
"ro.debuggable=0",
"ro.boot.verifiedbootstate=green",
"ro.boot.vbmeta.device_state=locked",
"ro.boot.flash.locked=1",
"ro.boot.veritymode=enforcing",
]
}
LAUNCH_PRESETS = {
"redroid-pro": {
"props": [
"androidboot.redroid_width=720",
"androidboot.redroid_height=1280",
"androidboot.redroid_dpi=320",
"androidboot.redroid_fps=30",
"androidboot.redroid_vm_heap=256m",
"ro.config.low_ram=true",
"config.disable_consumerir=true",
],
"device_profile": "pixel-3-xl",
"memory": "4g",
}
}
def infer_bridge(image, requested):
if requested != "auto":
return requested
if "houdini" in image:
return "libhoudini"
if "ndk" in image:
return "libndk"
return "none"
def bridge_props(mode):
if mode == "none":
return []
props = list(COMMON_BRIDGE_PROPS)
if mode == "libhoudini":
props.append("ro.dalvik.vm.native.bridge=libhoudini.so")
elif mode == "libnb":
props.append("ro.dalvik.vm.native.bridge=libnb.so")
elif mode == "libndk":
props.append("ro.dalvik.vm.native.bridge=libndk_translation.so")
props.append("ro.ndk_translation.version=0.2.3")
else:
raise ValueError("Unsupported bridge mode: {}".format(mode))
return props
def device_profile_props(profile):
if profile == "none":
return []
try:
return list(DEVICE_PROFILES[profile])
except KeyError as exc:
raise ValueError("Unsupported device profile: {}".format(profile)) from exc
def preset_props(preset):
if preset == "none":
return []
try:
return list(LAUNCH_PRESETS[preset]["props"])
except KeyError as exc:
raise ValueError("Unsupported preset: {}".format(preset)) from exc
def resolve_device_profile(preset, requested):
if requested != "none":
return requested
if preset == "none":
return "none"
return LAUNCH_PRESETS[preset].get("device_profile", "none")
def resolve_memory(preset, requested):
if requested:
return requested
if preset == "none":
return None
return LAUNCH_PRESETS[preset].get("memory")
def default_name(image):
name = image.split("/")[-1].replace(":", "_")
name = re.sub(r"[^a-zA-Z0-9_.-]+", "-", name).strip("-")
return name or "redroid"
def quote_cmd(args):
return " ".join(shlex.quote(part) for part in args)
def wait_tcp_port(host, port, timeout_sec, interval=1.0):
"""Return True once TCP connect succeeds, False on timeout."""
deadline = time.monotonic() + timeout_sec
while time.monotonic() < deadline:
try:
with socket.create_connection((host, port), timeout=2.0):
return True
except OSError:
time.sleep(interval)
return False
def main():
parser = argparse.ArgumentParser(
description="Launch a redroid container with a tested native bridge configuration."
)
parser.add_argument(
"--image",
required=True,
help="Image name, for example redroid/redroid:13.0.0_mindthegapps_houdini_magisk",
)
parser.add_argument(
"-c",
"--container",
default="docker",
choices=["docker", "podman"],
help="Container runtime",
)
parser.add_argument(
"--name",
help="Container name. Defaults to a slug derived from the image tag.",
)
parser.add_argument(
"--data-dir",
default="~/data-redroid",
help="Host directory mapped to /data",
)
parser.add_argument(
"--port",
type=int,
default=5555,
help="Host TCP port mapped to guest ADB 5555",
)
parser.add_argument(
"--bridge",
choices=["auto", "none", "libhoudini", "libnb", "libndk"],
default="auto",
help="Native bridge runtime property to inject",
)
parser.add_argument(
"--preset",
choices=["none"] + sorted(LAUNCH_PRESETS.keys()),
default="none",
help="Inject a tested launch preset",
)
parser.add_argument(
"--gpu-mode",
default="auto",
help=(
"androidboot.redroid_gpu_mode: auto=host if /dev/dri exists else guest (default); "
"guest=software GLES; host=GPU (adds --device for each DRI node when present)"
),
)
parser.add_argument(
"--device-profile",
choices=["none"] + sorted(DEVICE_PROFILES.keys()),
default="none",
help="Inject a tested device identity profile",
)
parser.add_argument(
"--memory",
help="Optional container memory limit, for example 4g",
)
parser.add_argument(
"--replace",
action="store_true",
help="Remove an existing container with the same name before running",
)
parser.add_argument(
"--keep-container",
action="store_true",
help="Do not pass --rm to the runtime",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print the generated command without executing it",
)
parser.add_argument(
"--extra-prop",
action="append",
default=[],
help="Additional redroid property, repeatable",
)
parser.add_argument(
"--adb-insecure",
action="store_true",
help=(
"Headless ADB: early androidboot.adb.secure=0, ro.adb.secure=0, ro.secure=0, ro.debuggable=1 "
"(before device profile; see redroid-doc README / #579). Overrides user-build secure props."
),
)
parser.add_argument(
"--no-compat-check",
action="store_true",
help="Skip compatibility messages and docker/podman probe (still resolves --gpu-mode auto from DRI)",
)
parser.add_argument(
"--wait-tcp",
type=int,
default=0,
metavar="SEC",
help=(
"After start, wait up to SEC seconds for 127.0.0.1:PORT to accept TCP (Android adbd). "
"If it never opens, print container logs. Use 90 when debugging failed adb connect."
),
)
args = parser.parse_args()
image = args.image
name = args.name or default_name(image)
data_dir = os.path.abspath(os.path.expanduser(args.data_dir))
bridge = infer_bridge(image, args.bridge)
preset = args.preset
device_profile = resolve_device_profile(preset, args.device_profile)
memory = resolve_memory(preset, args.memory)
os.makedirs(data_dir, exist_ok=True)
run_report, effective_gpu_mode, dri_device_args = compat.analyze_for_run(
args.container,
args.gpu_mode,
skip_check=args.no_compat_check,
)
if not args.no_compat_check:
compat.emit_compat_report(run_report)
if run_report.errors:
sys.exit(1)
cmd = [
args.container,
"run",
"-d",
"--privileged",
"--name",
name,
"-v",
"{}:/data".format(data_dir),
"-p",
"{}:5555".format(args.port),
]
if memory:
cmd.extend(["--memory", memory])
if not args.keep_container:
cmd.append("--rm")
cmd.extend(dri_device_args)
cmd.append(image)
# Init applies read-only props in order; later ro.adb.secure=0 is ignored if adb.secure
# was already set (redroid-doc #579). Put these immediately after the image name.
if args.adb_insecure:
# Must precede device_profile_props (ro.secure=1 / ro.debuggable=0): init keeps first ro.* value.
# redroid-doc README: ro.secure=0 yields working adb shell without RSA prompt.
cmd.extend(
[
"androidboot.adb.secure=0",
"ro.adb.secure=0",
"ro.secure=0",
"ro.debuggable=1",
]
)
cmd.append("androidboot.redroid_gpu_mode={}".format(effective_gpu_mode))
cmd.extend(bridge_props(bridge))
cmd.extend(preset_props(preset))
cmd.extend(device_profile_props(device_profile))
cmd.extend(args.extra_prop)
if args.replace and not args.dry_run:
subprocess.run(
[args.container, "rm", "-f", name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
print_color(
"Selected GPU mode: {} (androidboot.redroid_gpu_mode)".format(effective_gpu_mode),
bcolors.GREEN,
)
print_color("Selected native bridge: {}".format(bridge), bcolors.GREEN)
if preset != "none":
print_color("Selected launch preset: {}".format(preset), bcolors.GREEN)
if device_profile != "none":
print_color("Selected device profile: {}".format(device_profile), bcolors.GREEN)
if args.adb_insecure:
print_color(
"ADB insecure: injecting boot props immediately after image name "
"(check Command: line starts with androidboot.adb.secure=0 …).",
bcolors.GREEN,
)
print("Command:\n{}\n".format(quote_cmd(cmd)))
if args.dry_run:
return
subprocess.run(cmd, check=True)
print_color("Container started as {}".format(name), bcolors.GREEN)
if args.wait_tcp > 0:
print_color(
"Waiting up to {}s for 127.0.0.1:{} (adbd TCP) ...".format(args.wait_tcp, args.port),
bcolors.GREEN,
)
if not wait_tcp_port("127.0.0.1", args.port, args.wait_tcp):
print_color(
"Port {} never opened. Common with androidboot.redroid_gpu_mode=host when EGL/GPU fails inside "
"the guest — try: python run_redroid.py ... --gpu-mode guest (and drop DRI if needed).".format(
args.port
),
bcolors.RED,
)
print_color("Last logs from container {!r}:".format(name), bcolors.YELLOW)
subprocess.run([args.container, "logs", "--tail", "100", name], check=False)
print(
"\nIf the container already exited with --rm, logs are gone; re-run with "
"--keep-container (no --rm) to capture docker logs after a crash."
)
print("ADB: adb connect 127.0.0.1:{}".format(args.port))
if effective_gpu_mode == "host":
print(
"Note: host GPU mode can prevent full boot; if connect fails, use --gpu-mode guest or "
"--wait-tcp 90 to diagnose."
)
if not args.adb_insecure:
print(
"If the device shows as unauthorized, stop the container and re-run with --adb-insecure "
"(headless instances cannot tap the USB debugging authorization dialog)."
)
else:
print(
"If adb still shows unauthorized: stop the container, delete host folder "
"{}/misc/adb (persisted keys from an older boot), then start again. "
"Also try: adb kill-server && adb start-server".format(data_dir)
)
if __name__ == "__main__":
main()