Skip to content

Instantly share code, notes, and snippets.

@mentha
Last active May 2, 2024 14:30
Show Gist options
  • Save mentha/7153b4a9174ca270653493528e46945b to your computer and use it in GitHub Desktop.
Save mentha/7153b4a9174ca270653493528e46945b to your computer and use it in GitHub Desktop.
launch android x86 iso in qemu
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"]
#!/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()
#!/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()
#!/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()
[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
[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