Last active
May 2, 2024 14:30
-
-
Save mentha/7153b4a9174ca270653493528e46945b to your computer and use it in GitHub Desktop.
launch android x86 iso in qemu
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
FROM fedora | |
RUN --mount=type=cache,dst=/var/cache/dnf/ \ | |
sed -e '$ainstall_weak_deps=False' -e '$akeepcache=True' -i /etc/dnf/dnf.conf && \ | |
dnf install -y \ | |
android-tools \ | |
e2fsprogs \ | |
novnc \ | |
p7zip-plugins \ | |
python3 \ | |
qemu-device-display-virtio-gpu \ | |
qemu-device-display-virtio-gpu-gl \ | |
qemu-device-display-virtio-gpu-pci \ | |
qemu-device-display-virtio-gpu-pci-gl \ | |
qemu-system-x86-core \ | |
qemu-ui-egl-headless \ | |
qemu-ui-opengl \ | |
virtiofsd \ | |
; | |
RUN adb start-server | |
ADD qemu-android.py /usr/local/libexec/ | |
ADD portmux.py /usr/local/libexec/ | |
ADD init.py /usr/local/libexec/ | |
ENTRYPOINT ["/usr/local/libexec/init.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from argparse import ArgumentParser | |
import os | |
import subprocess as sp | |
import sys | |
def sd_sockets(): | |
SD_LISTEN_FDS_START = 3 | |
if 'LISTEN_PID' in os.environ and int(os.environ['LISTEN_PID']) == os.getpid(): | |
listen_fds = int(os.environ['LISTEN_FDS']) | |
return range(SD_LISTEN_FDS_START, SD_LISTEN_FDS_START + listen_fds) | |
return [] | |
def main(): | |
os.chdir(os.path.dirname(sys.argv[0])) | |
ap = ArgumentParser(add_help=False) | |
ap.add_argument('--idle-timeout', dest='idle_timeout') | |
ap.add_argument('--help', '-h', action='store_true') | |
a, emuargs = ap.parse_known_args() | |
if a.help: | |
ap.print_help() | |
sp.run(['./qemu-android.py', '-h']) | |
exit() | |
emucmd = [ | |
'./qemu-android.py', | |
'--publish', '5555', | |
'--virtiofsd-args', '--sandbox=chroot --modcaps=-mknod', | |
'--vnc', '/run/emu-vnc.sock', | |
'--novnc', ':81', | |
'--novnc-path', '/usr/share/novnc', | |
] + emuargs | |
muxcmd = [ | |
'./portmux.py', | |
'--listen', ':80', | |
'--vnc', '/run/emu-vnc.sock', | |
'--http', '127.0.0.1:81', | |
'--fallback', '127.0.0.1:5555', | |
] | |
if a.idle_timeout is not None: | |
muxcmd += [ | |
'--idle-timeout', a.idle_timeout, | |
'--idle-action', 'adb connect 127.0.0.1:5555; adb shell reboot -p', | |
] | |
sdsocks = list(sd_sockets()) | |
if sdsocks: | |
for s in sdsocks: | |
muxcmd += ['--listen', str(s)] | |
sp.Popen(['adb', 'start-server'], stdin=sp.DEVNULL, stdout=sp.DEVNULL, stderr=sp.DEVNULL) | |
sp.Popen(muxcmd, pass_fds=sdsocks) | |
os.execvp(emucmd[0], emucmd) | |
if __name__ == '__main__': | |
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from argparse import ArgumentParser | |
from contextlib import suppress | |
from typing import List, Optional | |
import asyncio | |
import importlib | |
import re | |
import socket | |
import subprocess as sp | |
import sys | |
qemu_android = importlib.import_module('qemu-android') | |
TcpOrUnix = qemu_android.TcpOrUnix | |
ServerSocket = qemu_android.ServerSocket | |
class PortMux: | |
BUFSIZE = 1024 * 16 | |
listen: List[socket.socket] | |
timeout: float | |
idle_timeout: Optional[int] | |
idle_action: Optional[str] | |
vnc: Optional[TcpOrUnix] | |
spice: Optional[TcpOrUnix] | |
http: Optional[TcpOrUnix] | |
fallback: Optional[TcpOrUnix] | |
def main(self): | |
a = ArgumentParser() | |
a.add_argument('--listen', type=ServerSocket, action='append', default=[]) | |
a.add_argument('--timeout', type=float, default=1.) | |
a.add_argument('--idle-timeout', dest='idle_timeout', type=int) | |
a.add_argument('--idle-action', dest='idle_action') | |
a.add_argument('--vnc', type=TcpOrUnix) | |
a.add_argument('--spice', type=TcpOrUnix) | |
a.add_argument('--http', type=TcpOrUnix) | |
a.add_argument('--fallback', type=TcpOrUnix) | |
a.parse_args(namespace=self) | |
asyncio.run(self.amain()) | |
idle_timeout_task = None | |
def idle_timeout_stop(self): | |
if self.idle_timeout_task: | |
self.idle_timeout_task.cancel() | |
self.idle_timeout_task = None | |
def idle_timeout_start(self): | |
if self.idle_timeout is None: | |
return | |
async def idle_task(): | |
await asyncio.sleep(self.idle_timeout) | |
sp.Popen(self.idle_action, shell=True, stdin=sp.DEVNULL) | |
self.idle_timeout_stop() | |
self.idle_timeout_task = asyncio.create_task(idle_task()) | |
async def amain(self): | |
servers = [] | |
for s in self.listen: | |
if s.family == socket.AF_UNIX: | |
servers.append(await asyncio.start_unix_server(self.handle_conn, sock=s)) | |
else: | |
servers.append(await asyncio.start_server(self.handle_conn, sock=s)) | |
self.idle_timeout_start() | |
await asyncio.gather(*[x.serve_forever() for x in servers]) | |
async def detect_upstream(self, reader, buf): | |
buf += await reader.read(self.BUFSIZE) | |
if self.spice and buf[:4] == b'REDQ': | |
return self.spice | |
elif self.http and b'\n' in buf and re.search(rb'HTTP/\d\.\d', buf.split(b'\n')[0]): | |
return self.http | |
return self.fallback | |
active_conn_count = 0 | |
async def handle_conn(self, reader, writer): | |
try: | |
self.idle_timeout_stop() | |
self.active_conn_count += 1 | |
buf = bytearray() | |
upstream = None | |
with suppress(asyncio.TimeoutError): | |
upstream = await asyncio.wait_for(self.detect_upstream(reader, buf), self.timeout) | |
if self.vnc and not buf: # timeout | |
upstream = self.vnc | |
if not upstream: | |
return | |
await self.proxy_upstream(reader, writer, buf, upstream) | |
except ConnectionResetError: | |
pass | |
finally: | |
writer.close() | |
self.active_conn_count -= 1 | |
if self.active_conn_count == 0: | |
self.idle_timeout_start() | |
async def proxy_upstream(self, reader, writer, buf, upstream): | |
ur, uw = None, None | |
if upstream.unix: | |
ur, uw = await asyncio.open_unix_connection(upstream.unix) | |
else: | |
ur, uw = await asyncio.open_connection(upstream.host or 'localhost', upstream.port) | |
try: | |
uw.write(buf) | |
await uw.drain() | |
await asyncio.gather( | |
self.proxy_stream(reader, uw), | |
self.proxy_stream(ur, writer), | |
return_exceptions=True) | |
finally: | |
uw.close() | |
async def proxy_stream(self, reader, writer): | |
while True: | |
b = await reader.read(self.BUFSIZE) | |
sys.stdout.flush() | |
if not b: | |
if writer.can_write_eof(): | |
writer.write_eof() | |
else: | |
writer.close() | |
await writer.wait_closed() | |
break | |
writer.write(b) | |
await writer.drain() | |
if __name__ == '__main__': | |
PortMux().main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from argparse import ArgumentParser | |
from contextlib import ExitStack, suppress | |
from hashlib import md5 | |
from typing import List, Optional, Union | |
from warnings import warn | |
import html | |
import json | |
import math | |
import os | |
import re | |
import shlex | |
import signal | |
import socket | |
import stat | |
import subprocess as sp | |
import sys | |
import tempfile | |
class TcpOrUnix: | |
METAVAR = '([HOST:]PORT)|PATH' | |
def __init__(self, s): | |
self.host, self.port, self.unix = re.match( | |
r'^(?:(?:([\w.]*):)?(\d+)|([^,]+))$', s).groups() | |
if self.port: | |
self.host = self.host or '' | |
def format(self, default_host='127.0.0.1'): | |
if self.unix: | |
return self.unix | |
else: | |
return (self.host or default_host) + ':' + self.port | |
def __repr__(self): | |
return self.format() | |
def ServerSocket(s, a=None): | |
fd, host, port, unix = None, None, None, None | |
if a is None: | |
fd, host, port, unix = re.match( | |
r'^(?:(\d+)|(?:([\w.]*):(\d+))|(.+))$', s).groups() | |
else: | |
host, port, unix = a.host, a.port, a.unix | |
if fd: | |
fd = socket.socket(fileno=int(fd)) | |
elif port: | |
af, _, _, _, addr = socket.getaddrinfo(host or '::', port)[0] | |
fd = socket.create_server(addr, family=af, dualstack_ipv6=True) | |
else: | |
fd = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
fd.bind(unix) | |
fd.listen(5) | |
return fd | |
ServerSocket.METAVAR = 'FD|([HOST]:PORT)|PATH' | |
class PublishRule: | |
METAVAR = '[[HOST:][PORT]:]GUESTPORT[/PROTOCOL]' | |
def __init__(self, s): | |
self.host, self.port, self.guestport, self.protocol = re.match( | |
r'^(?:(?:(.*):)?(\d+)?:)?(\d+)(?:/(\w+))?$', s.lower()).groups() | |
self.host = self.host or '' | |
if self.host.startswith('[') and self.host.endswith(']'): | |
self.host = self.host[1:-1] | |
self.port = self.port or self.guestport | |
self.protocol = self.protocol or 'tcp' | |
class Resolution: | |
METAVAR = 'WxH[@DPI]' | |
def __init__(self, s): | |
self.width, self.height, self.dpi = re.match(r'^(\d+)x(\d+)(?:@(\d+))?$', s.lower()).groups() | |
def parseFilePath(arg): | |
r = os.path.abspath(arg) | |
if arg.endswith('/') and not r.endswith('/'): | |
r += '/' | |
return r | |
def parseByteSize(blocksize, defaultunit=None): | |
def parse(arg): | |
scales = { | |
'k': 1024, | |
'kib': 1024, | |
'kb': 1000, | |
'm': 1024**2, | |
'mib': 1024**2, | |
'mb': 1000**2, | |
'g': 1024**3, | |
'gib': 1024**3, | |
'gb': 1000**3, | |
} | |
m = re.match(r'^\s*([.\d]+)\s*(\w*)\s*$', arg) | |
unit = (m[2] or defaultunit or '').lower() | |
r = float(m[1]) * scales.get(unit, 1) | |
return math.ceil(r / blocksize) * blocksize | |
return parse | |
class QemuAndroid: | |
def __init__(self): | |
self.cleanup = ExitStack() | |
self.rundir = self.cleanup.enter_context(tempfile.TemporaryDirectory(prefix=f'qemu-android-{os.getpid()}-')) | |
self.subprocs = [] | |
self.qemu_fds = [] | |
self.qemu_cmdline = [] | |
self.qemu_env = {} | |
self.kernel_cmdline = [] | |
vmname: Optional[str] | |
mem_size: int | |
core_count: int | |
enable_headless: bool | |
render_node: Union[None, str, bool] | |
vnc_port: Optional[TcpOrUnix] | |
novnc_port: Optional[TcpOrUnix] | |
novnc_path: Optional[str] | |
disable_tablet: bool | |
use_ac97: bool | |
bootiso: bool | |
databackend: str | |
datasize: int | |
virtiofsd_args: Optional[str] | |
isolated_network: bool | |
publishs: List[PublishRule] | |
gdb_port: int | |
kernel: Optional[str] | |
initrd: Optional[str] | |
append: Optional[str] | |
use_virtwifi: bool | |
enable_permissive: bool | |
resolution: Resolution | |
iso_path: str | |
data_path: Optional[str] | |
def parse_args(self): | |
a = ArgumentParser(description='run android-x86 in QEMU') | |
a.add_argument('--name', dest='vmname', metavar='NAME', | |
help='VM name') | |
a.add_argument('--mem', dest='mem_size', metavar='SIZE[MiB]', | |
type=parseByteSize(1024**2, 'mib'), default=4 * 1024**3, help='memory size') | |
a.add_argument('--smp', dest='core_count', metavar='N', | |
type=int, default=4, help='cpu core count') | |
a.add_argument('--headless', dest='enable_headless', | |
action='store_true', help='run without gui') | |
a.add_argument('--render-node', dest='render_node', | |
default=True, help='specify render node') | |
a.add_argument('--no-gpu', dest='render_node', | |
action='store_const', const=None, help='disable hardware rendering') | |
a.add_argument('--vnc', dest='vnc_port', metavar=TcpOrUnix.METAVAR, | |
type=TcpOrUnix, help='enable vnc') | |
a.add_argument('--novnc', dest='novnc_port', metavar=TcpOrUnix.METAVAR, | |
type=TcpOrUnix, help='enable novnc') | |
a.add_argument('--novnc-path', dest='novnc_path', metavar='PATH', | |
help='path to novnc') | |
a.add_argument('--no-tablet', dest='disable_tablet', | |
action='store_true', help='disable tablet') | |
a.add_argument('--ac97', dest='use_ac97', | |
action='store_true', help='emulate AC97 sound') | |
a.add_argument('--bootiso', | |
action='store_true', help='boot from iso bootloader') | |
a.add_argument('--databackend', | |
default='auto', choices=('auto', 'none', 'image', 'virtiofs')) | |
a.add_argument('--datasize', | |
type=parseByteSize(1024**2, 'gib'), default=64 * 1024**3, help='data disk size') | |
a.add_argument('--virtiofsd-args', dest='virtiofsd_args', metavar='ARGS', | |
help='extra options to virtiofsd') | |
a.add_argument('--isolated', dest='isolated_network', | |
action='store_true', help='block all connections except for explicit forwardings') | |
a.add_argument('--publish', '-p', dest='publishs', metavar=PublishRule.METAVAR, | |
action='append', default=[], type=PublishRule, help='forward guest port') | |
a.add_argument('--gdb', dest='gdb_port', metavar='PORT', | |
type=int, help='wait for gdb on specified port') | |
a.add_argument('--kernel', | |
help='custom kernel') | |
a.add_argument('--initrd', | |
help='custom initrd') | |
a.add_argument('--append', | |
help='extra parameters to kernel commandline') | |
a.add_argument('--virtwifi', dest='use_virtwifi', | |
action='store_true', help='enable virt wifi') | |
a.add_argument('--permissive', dest='enable_permissive', | |
action='store_true', help='enable selinux permissive mode') | |
a.add_argument('--resolution', '-r', metavar=Resolution.METAVAR, | |
type=Resolution, default=Resolution('1280x720@160'), help='set guest resolution and dpi') | |
a.add_argument('iso_path', metavar='ISO', | |
type=parseFilePath, help='android x86 iso') | |
a.add_argument('data_path', metavar='DATA', | |
type=parseFilePath, nargs='?', help='data disk image or dir path') | |
a.parse_args(namespace=self) | |
def prepare_basic(self): | |
if self.vmname is None: | |
self.vmname = os.path.splitext(os.path.basename((self.data_path or 'ephemeral').rstrip('/')))[0] | |
self.qemu_cmdline += [ | |
'qemu-system-x86_64', | |
'-name', self.vmname, | |
'-object', f'memory-backend-memfd,id=mem,size={self.mem_size // 1024**2}M,share=on', | |
'-machine', 'q35,memory-backend=mem', | |
'-cpu', 'host', | |
'-accel', 'kvm', | |
'-smp', f'cores={self.core_count}', | |
'-serial', 'mon:telnet::23,server=on,wait=off', | |
'-device', 'virtio-balloon', | |
'-device', 'virtio-rng', | |
'-device', 'virtio-keyboard', | |
] | |
self.kernel_cmdline += [ | |
#'quiet', | |
'console=ttyS0', | |
'root=/dev/ram0', | |
'SRC=', | |
'mem_sleep_default=shallow', | |
'SET_SCREEN_OFF_TIMEOUT=0', | |
'androidboot.fake_battery=true', | |
] | |
@staticmethod | |
def find_vhost_user(typ, raise_error=True): | |
vhostdir = '/usr/share/qemu/vhost-user' | |
if os.path.isdir(vhostdir): | |
for e in os.listdir(vhostdir): | |
with open(os.path.join(vhostdir, e)) as f: | |
f = json.load(f) | |
if 'binary' in f and f.get('type') == typ: | |
return f['binary'] | |
if not raise_error: | |
return None | |
raise RuntimeError(f'vhost-user backend for {typ} not found') | |
@staticmethod | |
def find_render_node(): | |
for e in os.listdir('/dev/dri'): | |
if re.match(r'^renderD\d+$', e): | |
return os.path.join('/dev/dri', e) | |
return None | |
def spawn(self, *a, null_stdin=True, env=None, **ka): | |
if null_stdin: | |
ka.setdefault('stdin', sp.DEVNULL) | |
if env: | |
newenv = os.environ.copy() | |
for k, v in env.items(): | |
if v is None: | |
if k in newenv: | |
del newenv[k] | |
else: | |
newenv[k] = v | |
ka['env'] = newenv | |
p = sp.Popen(*a, **ka) | |
self.cleanup.enter_context(p) | |
self.subprocs.append(p) | |
return p | |
def prepare_display(self): | |
if self.novnc_port: | |
self.vnc_port = self.vnc_port or os.path.join(self.rundir, 'vnc.sock') | |
if self.vnc_port: | |
self.enable_headless = True | |
if self.render_node is True: | |
self.render_node = self.find_render_node() | |
# audio | |
if self.enable_headless: | |
self.qemu_cmdline += [ | |
'-audiodev', 'none,id=audio', | |
] | |
else: | |
self.qemu_cmdline += [ | |
'-audiodev', 'sdl,id=audio', | |
] | |
if self.use_ac97: | |
self.qemu_cmdline += [ | |
'-device', 'AC97,audiodev=audio', | |
] | |
else: | |
self.qemu_cmdline += [ | |
'-device', 'virtio-sound,audiodev=audio', | |
] | |
# gpu and display | |
if self.render_node: | |
vhostgpu = self.find_vhost_user('gpu', False) | |
if self.enable_headless: | |
vhostgpu = None | |
if vhostgpu: | |
gpu_sock = os.path.join(self.rundir, 'vhost-gpu.sock') | |
self.spawn([ | |
vhostgpu, | |
'--socket-path', gpu_sock, | |
'--render-node', self.render_node, | |
'--virgl', | |
]) | |
self.qemu_cmdline += [ | |
'-vga', 'none', | |
'-chardev', 'socket,id=vgpu,path=' + gpu_sock, | |
'-device', 'vhost-user-gpu-pci,chardev=vgpu', | |
] | |
else: | |
warn('cannot use vhost-user-gpu, vm performance will be impacted') | |
self.qemu_cmdline += [ | |
'-vga', 'none', | |
'-device', 'virtio-gpu-gl', | |
] | |
if self.enable_headless: | |
if vhostgpu: | |
# wait for qemu support | |
raise NotImplementedError() | |
else: | |
self.qemu_cmdline += [ | |
'-display', 'egl-headless,rendernode=' + self.render_node, | |
] | |
else: | |
self.qemu_cmdline += [ | |
'-display', 'gtk,gl=on,show-tabs=off,window-close=on', | |
] | |
self.kernel_cmdline += [ | |
'HWC=drm_minigbm', | |
'GRALLOC=minigbm_arcvm', | |
'OMX_NO_YUV420=1', | |
'CODEC2_LEVEL=0', | |
#'ANGLE=1', virgl does not support vulkan | |
] | |
else: | |
self.qemu_cmdline += [ | |
'-vga', 'none', | |
'-device', 'virtio-gpu', | |
] | |
self.kernel_cmdline += [ | |
'HWACCEL=0', | |
#'ANGLE=1', | |
] | |
if self.enable_headless: | |
self.qemu_cmdline += [ | |
'-display', 'none', | |
] | |
else: | |
self.qemu_cmdline += [ | |
'-display', 'gtk,show-tabs=off,window-close=on', | |
] | |
if self.vnc_port: | |
p = None | |
if self.vnc_port.unix: | |
p = 'unix:' + self.vnc_port.unix | |
else: | |
port = int(self.vnc_port.port) - 5900 | |
if port < 0: | |
raise RuntimeError('invalid vnc port') | |
p = f'{self.vnc_port.host}:{port}' | |
p += ',audiodev=audio,power-control=on' | |
# ,lossy=on # wait for novnc PR#1855 | |
self.qemu_cmdline += [ | |
'-vnc', p, | |
] | |
# devices | |
if self.disable_tablet: | |
self.qemu_cmdline += [ | |
'-device', 'virtio-mouse', | |
] | |
else: | |
self.qemu_cmdline += [ | |
'-device', 'virtio-tablet', | |
] | |
# novnc | |
if self.novnc_port: | |
if not self.novnc_path: | |
raise RuntimeError('novnc path not specified') | |
webroot = os.path.join(self.rundir, 'novnc-webroot') | |
os.makedirs(webroot) | |
os.symlink(self.novnc_path, os.path.join(webroot, 'novnc'), True) | |
with open(os.path.join(webroot, 'index.html'), 'w') as f: | |
f.write('<!DOCTYPE html>\n' | |
'<html>' | |
'<head>' | |
f'<title>{html.escape(self.vmname)}</title>' | |
'<meta name="viewport" content="width=device-width, initial-scale=1"/>' | |
'<style>' | |
'#vnc {' | |
'position: fixed;' | |
'left: 0;' | |
'top: 0;' | |
'width: 100%;' | |
'height: 100%;' | |
'}' | |
'</style>' | |
'</head>' | |
'<body>' | |
'<iframe id="vnc" allowfullscreen="true"></iframe>' | |
'<script>' | |
'document.getElementById("vnc").src = "novnc/?path=" + encodeURIComponent((new URL("websockify", document.location)).pathname) + "&autoconnect=1&reconnect=1&reconnect_delay=500&resize=scale&view_only=1&show_dot=1";' | |
'</script>' | |
'</body>' | |
'</html>') | |
novnc_cmd = [ | |
'websockify', | |
'--web=' + webroot, | |
'--inetd', | |
] | |
if self.vnc_port.unix: | |
novnc_cmd += ['--unix-target=' + self.vnc_port.unix] | |
else: | |
novnc_cmd += [f'{self.vnc_port.host}:{self.vnc_port.port}'] | |
with ServerSocket(None, self.novnc_port) as lsock: | |
self.spawn(novnc_cmd, stdin=lsock, stdout=sp.DEVNULL, stderr=sp.DEVNULL) | |
def prepare_disk(self): | |
self.qemu_cmdline += [ | |
'-drive', f'file={self.iso_path},if=none,id=iso,format=raw,read-only=on', | |
] | |
if self.bootiso: | |
self.qemu_cmdline += [ | |
'-device', 'virtio-scsi-pci,id=scsi0', | |
'-device', 'scsi-cd,drive=iso,bus=scsi0.0', | |
'-boot', 'order=d', | |
] | |
else: | |
self.qemu_cmdline += [ | |
'-device', 'virtio-blk,drive=iso', | |
] | |
if self.databackend == 'auto': | |
if self.data_path is None: | |
self.databackend = 'none' | |
elif self.data_path.endswith('/') or os.path.isdir(self.data_path): | |
self.databackend = 'virtiofs' | |
else: | |
self.databackend = 'image' | |
if self.databackend == 'none': | |
self.databackend = 'image' | |
self.data_path = os.path.join(self.rundir, 'disk.img') | |
if self.databackend == 'image': | |
if not os.path.exists(self.data_path): | |
with open(self.data_path, 'wb') as f: | |
os.fchmod(f.fileno(), 0o600) | |
else: | |
with open(self.data_path, 'r+b') as f: | |
pass | |
st = os.stat(self.data_path) | |
if not stat.S_ISREG(st.st_mode): | |
raise RuntimeError('data image is not regular file') | |
if st.st_size < self.datasize: | |
os.truncate(self.data_path, self.datasize) | |
if st.st_size == 0: | |
os.chmod(self.data_path, 0o600) | |
sp.run(['mke2fs', '-q', '-L', 'data', self.data_path], stdout=sp.DEVNULL, check=True) | |
else: | |
sp.run(['resize2fs', self.data_path], stdout=sp.DEVNULL, check=True) | |
#if sp.run(['e2fsck', '-y', self.data_path], stdout=sp.DEVNULL).returncode not in {0, 1}: | |
# raise RuntimeError('e2fsck failed') | |
self.qemu_cmdline += [ | |
'-drive', f'file={self.data_path},if=none,id=data,format=raw,discard=on', | |
'-device', 'virtio-blk,drive=data', | |
] | |
self.kernel_cmdline += ['DATA=LABEL=data'] | |
elif self.databackend == 'virtiofs': | |
os.makedirs(self.data_path, exist_ok=True) | |
try: | |
if 'user.test_xattr' not in os.listxattr(self.data_path): | |
os.setxattr(self.data_path, 'user.test_xattr', b'test') | |
except OSError: | |
raise RuntimeError('data dir missing xattr support') | |
vfs_sock = os.path.join(self.rundir, 'virtiofsd.sock') | |
vfs_cmd = [ # FIXME: doesn't work with android-x86 esdfs mounted /storage/emulated | |
self.find_vhost_user('fs'), | |
'--shared-dir', self.data_path, | |
'--socket-path', vfs_sock, | |
'--xattr', | |
'--posix-acl', | |
'--xattrmap', ':prefix:all::user.virtiofs.:', | |
'--no-announce-submounts', | |
] | |
if self.virtiofsd_args: | |
vfs_cmd += shlex.split(self.virtiofsd_args) | |
if os.getuid() != 0: | |
raise RuntimeError('virtiofs does not work without root') | |
self.spawn(vfs_cmd) | |
self.qemu_cmdline += [ | |
'-chardev', 'socket,id=vfs,path=' + vfs_sock, | |
'-device', 'vhost-user-fs-pci,chardev=vfs,queue-size=1024,tag=data', | |
] | |
self.kernel_cmdline += ['DATA=virtiofs'] | |
else: | |
raise RuntimeError('invalid data backend') | |
def prepare_net(self): | |
usernet = '' | |
if self.isolated_network: | |
usernet += ',restrict=on' | |
for p in self.publishs: | |
usernet += f',hostfwd={p.protocol}:{p.host}:{p.port}-:{p.guestport}' | |
r = md5(self.vmname.encode()).hexdigest() | |
self.qemu_cmdline += [ | |
'-netdev', 'user,id=net' + usernet, | |
'-device', f'virtio-net,netdev=net,mac=52:54:00:{r[0:2]}:{r[2:4]}:{r[4:6]}', | |
] | |
def extract_iso_file(self, path, name=None): | |
p = os.path.join(self.rundir, name or path) | |
with open(p, 'wb') as f: | |
sp.run(['7z', 'e', '-tISO', '-so', self.iso_path, path], stdout=f, check=True) | |
return p | |
def prepare_kernel(self): | |
if self.gdb_port: | |
self.qemu_cmdline += [ | |
'-gdb', f'tcp::{self.gdb_port}', | |
'-S', | |
] | |
if self.bootiso: | |
return | |
kernel = self.kernel or self.extract_iso_file('kernel') | |
initrd = self.initrd or self.extract_iso_file('initrd.img') | |
self.qemu_cmdline += [ | |
'-kernel', kernel, | |
'-initrd', initrd, | |
] | |
if self.use_virtwifi: | |
self.kernel_cmdline += ['VIRT_WIFI=1'] | |
if self.enable_permissive: | |
self.kernel_cmdline += ['enforcing=0'] | |
if self.resolution: | |
self.kernel_cmdline += [f'video={self.resolution.width}x{self.resolution.height}'] | |
if self.resolution.dpi is not None: | |
self.kernel_cmdline += [f'DPI={self.resolution.dpi}'] | |
append = ' '.join(self.kernel_cmdline) | |
if self.append: | |
append += ' ' + self.append | |
self.qemu_cmdline += [ | |
'-append', append, | |
] | |
def setup_signal(self): | |
def exit_sig(*_): | |
sys.exit() | |
for s in ( | |
signal.SIGHUP, | |
signal.SIGINT, | |
signal.SIGTERM, | |
): | |
signal.signal(s, exit_sig) | |
def main(self): | |
self.parse_args() | |
self.setup_signal() | |
with self.cleanup: | |
try: | |
self.prepare_basic() | |
self.prepare_display() | |
self.prepare_disk() | |
self.prepare_net() | |
self.prepare_kernel() | |
p = self.spawn(self.qemu_cmdline, null_stdin=False, env=self.qemu_env, pass_fds=self.qemu_fds) | |
sys.exit(p.wait()) | |
finally: | |
for p in self.subprocs: | |
with suppress(Exception): | |
p.terminate() | |
if __name__ == '__main__': | |
QemuAndroid().main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Service] | |
#Environment=IMAGE_NAME_BASE= | |
#Environment=ISO_PATH= | |
#Environment=COMMON_EXTRA_OPTS= | |
#Environment=EXTRA_OPTS= | |
EnvironmentFile=/usr/local/etc/qemu-android/env | |
EnvironmentFile=-/usr/local/etc/qemu-android/%i.env | |
Environment=PUB_PORT=%i | |
ExecStartPre=touch ${IMAGE_NAME_BASE}-${PUB_PORT}.img | |
ExecStartPre=chmod 600 ${IMAGE_NAME_BASE}-${PUB_PORT}.img | |
ExecStart=podman run \ | |
--rm --replace \ | |
--read-only \ | |
--read-only-tmpfs \ | |
--name %p-${PUB_PORT} \ | |
--network=slirp4netns \ | |
--userns=auto \ | |
--volume=qemu-android-shader-cache-${PUB_PORT}:/mnt/shader-cache:rw,U \ | |
--env=MESA_SHADER_CACHE_DIR=/mnt/shader-cache \ | |
--device=/dev/kvm \ | |
--device=/dev/dri \ | |
--volume=${ISO_PATH}:/android.iso:ro \ | |
--volume=${IMAGE_NAME_BASE}-${PUB_PORT}.img:/data.img:rw,U \ | |
qemu-android:latest \ | |
/android.iso \ | |
/data.img \ | |
$COMMON_EXTRA_OPTS \ | |
$EXTRA_OPTS |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Socket] | |
ListenStream=127.0.0.1:%i | |
[Install] | |
WantedBy=default.target |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment