#!/usr/bin/env python3 """ CLeonOS-Wine (Qiling backend) Run CLeonOS x86_64 user ELF binaries on host OSes with a lightweight syscall shim. """ from __future__ import annotations import argparse import collections import os import struct import sys import threading import time from dataclasses import dataclass, field from pathlib import Path from typing import Deque, Optional, Tuple try: from qiling import Qiling except Exception as exc: print("[WINE][ERROR] qiling import failed. Install dependencies first:", file=sys.stderr) print(" pip install -r wine/requirements.txt", file=sys.stderr) raise SystemExit(1) from exc try: from unicorn import UC_PROT_EXEC, UC_PROT_READ, UC_PROT_WRITE except Exception: UC_PROT_READ = 1 UC_PROT_WRITE = 2 UC_PROT_EXEC = 4 U64_MASK = (1 << 64) - 1 MAX_CSTR = 4096 MAX_IO_READ = 1 << 20 DEFAULT_MAX_EXEC_DEPTH = 6 FS_NAME_MAX = 96 # CLeonOS syscall IDs from cleonos/c/include/cleonos_syscall.h SYS_LOG_WRITE = 0 SYS_TIMER_TICKS = 1 SYS_TASK_COUNT = 2 SYS_CUR_TASK = 3 SYS_SERVICE_COUNT = 4 SYS_SERVICE_READY_COUNT = 5 SYS_CONTEXT_SWITCHES = 6 SYS_KELF_COUNT = 7 SYS_KELF_RUNS = 8 SYS_FS_NODE_COUNT = 9 SYS_FS_CHILD_COUNT = 10 SYS_FS_GET_CHILD_NAME = 11 SYS_FS_READ = 12 SYS_EXEC_PATH = 13 SYS_EXEC_REQUESTS = 14 SYS_EXEC_SUCCESS = 15 SYS_USER_SHELL_READY = 16 SYS_USER_EXEC_REQUESTED = 17 SYS_USER_LAUNCH_TRIES = 18 SYS_USER_LAUNCH_OK = 19 SYS_USER_LAUNCH_FAIL = 20 SYS_TTY_COUNT = 21 SYS_TTY_ACTIVE = 22 SYS_TTY_SWITCH = 23 SYS_TTY_WRITE = 24 SYS_TTY_WRITE_CHAR = 25 SYS_KBD_GET_CHAR = 26 def u64(value: int) -> int: return value & U64_MASK def u64_neg1() -> int: return U64_MASK @dataclass class SharedKernelState: start_ns: int = field(default_factory=time.monotonic_ns) task_count: int = 5 current_task: int = 0 service_count: int = 7 service_ready: int = 7 context_switches: int = 0 kelf_count: int = 2 kelf_runs: int = 0 exec_requests: int = 0 exec_success: int = 0 user_shell_ready: int = 1 user_exec_requested: int = 0 user_launch_tries: int = 0 user_launch_ok: int = 0 user_launch_fail: int = 0 tty_count: int = 4 tty_active: int = 0 kbd_queue: Deque[int] = field(default_factory=collections.deque) kbd_lock: threading.Lock = field(default_factory=threading.Lock) def timer_ticks(self) -> int: # Millisecond style tick is enough for shell/user utilities. return (time.monotonic_ns() - self.start_ns) // 1_000_000 def push_key(self, key: int) -> None: with self.kbd_lock: if len(self.kbd_queue) >= 1024: self.kbd_queue.popleft() self.kbd_queue.append(key & 0xFF) def pop_key(self) -> Optional[int]: with self.kbd_lock: if not self.kbd_queue: return None return self.kbd_queue.popleft() class InputPump: def __init__(self, state: SharedKernelState) -> None: self.state = state self._stop = threading.Event() self._thread: Optional[threading.Thread] = None self._posix_term_state = None def start(self) -> None: if self._thread is not None: return if not sys.stdin or not hasattr(sys.stdin, "isatty") or not sys.stdin.isatty(): return self._thread = threading.Thread(target=self._run, name="cleonos-wine-input", daemon=True) self._thread.start() def stop(self) -> None: self._stop.set() if self._thread is not None: self._thread.join(timeout=0.2) self._thread = None self._restore_posix_tty() def _run(self) -> None: if os.name == "nt": self._run_windows() else: self._run_posix() def _run_windows(self) -> None: import msvcrt # pylint: disable=import-error while not self._stop.is_set(): if not msvcrt.kbhit(): time.sleep(0.005) continue ch = msvcrt.getwch() if ch in ("\x00", "\xe0"): # Extended scan code second byte, ignore for now. _ = msvcrt.getwch() continue norm = self._normalize_char(ch) if norm is None: continue self.state.push_key(ord(norm)) def _run_posix(self) -> None: import select import termios import tty fd = sys.stdin.fileno() self._posix_term_state = termios.tcgetattr(fd) tty.setcbreak(fd) try: while not self._stop.is_set(): readable, _, _ = select.select([sys.stdin], [], [], 0.05) if not readable: continue ch = sys.stdin.read(1) norm = self._normalize_char(ch) if norm is None: continue self.state.push_key(ord(norm)) finally: self._restore_posix_tty() def _restore_posix_tty(self) -> None: if self._posix_term_state is None: return try: import termios fd = sys.stdin.fileno() termios.tcsetattr(fd, termios.TCSADRAIN, self._posix_term_state) except Exception: pass finally: self._posix_term_state = None @staticmethod def _normalize_char(ch: str) -> Optional[str]: if not ch: return None if ch == "\r": return "\n" return ch class CLeonOSWine: def __init__( self, elf_path: Path, rootfs: Path, guest_path_hint: str, *, state: Optional[SharedKernelState] = None, depth: int = 0, max_exec_depth: int = DEFAULT_MAX_EXEC_DEPTH, no_kbd: bool = False, verbose: bool = False, top_level: bool = True, ) -> None: self.elf_path = elf_path self.rootfs = rootfs self.guest_path_hint = guest_path_hint self.state = state if state is not None else SharedKernelState() self.depth = depth self.max_exec_depth = max_exec_depth self.no_kbd = no_kbd self.verbose = verbose self.top_level = top_level self.entry = self._read_elf_entry(elf_path) self.exit_code: Optional[int] = None self._input_pump: Optional[InputPump] = None self._stack_base = 0x00007FFF00000000 self._stack_size = 0x0000000000020000 self._ret_sentinel = self._stack_base + 0x1000 def run(self) -> Optional[int]: if self.entry == 0: print(f"[WINE][ERROR] ELF entry is 0, cannot run: {self.elf_path}", file=sys.stderr) return None ql = self._new_ql() self._install_hooks(ql) self._prepare_custom_stack(ql) if self.top_level and not self.no_kbd: self._input_pump = InputPump(self.state) self._input_pump.start() try: try: ql.run(begin=self.entry) except TypeError: ql.run() except Exception as exc: if self.verbose or self.top_level: print(f"[WINE][ERROR] runtime crashed: {exc}", file=sys.stderr) return None finally: if self.top_level and self._input_pump is not None: self._input_pump.stop() if self.exit_code is None: self.exit_code = self._read_reg(ql, "rax") return u64(self.exit_code) def _new_ql(self) -> Qiling: kwargs = {} level = self._resolve_qiling_verbose(self.verbose) if level is not None: kwargs["verbose"] = level try: return Qiling([str(self.elf_path)], str(self.rootfs), **kwargs) except Exception as exc: if self.verbose or self.top_level: print(f"[WINE][WARN] native ELF loader failed, fallback to BLOB mode: {exc}", file=sys.stderr) return self._new_ql_blob(kwargs) def _new_ql_blob(self, kwargs: dict) -> Qiling: arch, ostype = self._resolve_blob_arch_os() last_error: Optional[Exception] = None creators = ( lambda: Qiling([], str(self.rootfs), ostype=ostype, archtype=arch, **kwargs), lambda: Qiling(argv=[], rootfs=str(self.rootfs), ostype=ostype, archtype=arch, **kwargs), lambda: Qiling(code=b"\x90", rootfs=str(self.rootfs), ostype=ostype, archtype=arch, **kwargs), ) ql = None for create in creators: try: ql = create() break except Exception as exc: last_error = exc if ql is None: raise RuntimeError(f"unable to create qiling blob instance: {last_error}") self._load_elf_segments_into_ql(ql, self.elf_path) return ql @staticmethod def _resolve_qiling_verbose(user_verbose: bool): try: from qiling.const import QL_VERBOSE # pylint: disable=import-outside-toplevel except Exception: return None if user_verbose: for name in ("DEBUG", "DEFAULT"): if hasattr(QL_VERBOSE, name): return getattr(QL_VERBOSE, name) return None for name in ("DISABLED", "OFF", "DEFAULT"): if hasattr(QL_VERBOSE, name): return getattr(QL_VERBOSE, name) return None @staticmethod def _resolve_blob_arch_os() -> Tuple[object, object]: from qiling.const import QL_ARCH, QL_OS # pylint: disable=import-outside-toplevel arch = None for name in ("X8664", "X86_64", "X86_64BITS"): if hasattr(QL_ARCH, name): arch = getattr(QL_ARCH, name) break ostype = None for name in ("BLOB", "NONE"): if hasattr(QL_OS, name): ostype = getattr(QL_OS, name) break if arch is None or ostype is None: raise RuntimeError("qiling BLOB arch/os constants not found") return arch, ostype def _load_elf_segments_into_ql(self, ql: Qiling, path: Path) -> None: data = path.read_bytes() if len(data) < 64 or data[0:4] != b"\x7fELF" or data[4] != 2 or data[5] != 1: raise RuntimeError(f"unsupported ELF format: {path}") phoff = struct.unpack_from(" len(data): break p_type, p_flags, p_offset, p_vaddr, _p_paddr, p_filesz, p_memsz, _p_align = struct.unpack_from( " 0: file_start = int(p_offset) file_end = min(len(data), file_start + int(p_filesz)) if file_start < file_end: seg_data = data[file_start:file_end] if not self._safe_write_mem(ql, int(p_vaddr), seg_data): raise RuntimeError(f"failed to write PT_LOAD segment at 0x{int(p_vaddr):X}") perms = 0 if p_flags & 0x4: perms |= UC_PROT_READ if p_flags & 0x2: perms |= UC_PROT_WRITE if p_flags & 0x1: perms |= UC_PROT_EXEC if perms == 0: perms = UC_PROT_READ try: ql.mem.protect(map_start, map_size, perms) except Exception: pass loaded += 1 if loaded == 0: raise RuntimeError(f"no PT_LOAD segments found: {path}") def _install_hooks(self, ql: Qiling) -> None: hooker = getattr(ql, "hook_intno", None) if hooker is None: hooker = getattr(ql, "hook_intr", None) if hooker is None: raise RuntimeError("Qiling interrupt hook API not found") installed = False for fn in ( lambda: hooker(self._on_int80, 0x80), lambda: hooker(0x80, self._on_int80), ): try: fn() installed = True break except TypeError: continue if not installed: raise RuntimeError("Failed to install INT 0x80 hook") hook_addr = getattr(ql, "hook_address", None) if hook_addr is None: raise RuntimeError("Qiling address hook API not found") installed = False for fn in ( lambda: hook_addr(self._on_entry_return, self._ret_sentinel), lambda: hook_addr(self._ret_sentinel, self._on_entry_return), ): try: fn() installed = True break except TypeError: continue if not installed: raise RuntimeError("Failed to install return sentinel hook") def _prepare_custom_stack(self, ql: Qiling) -> None: self._map_memory(ql, self._stack_base, self._stack_size) if not self._safe_write_mem(ql, self._ret_sentinel, b"\xC3"): raise RuntimeError("failed to place return sentinel") rsp = self._stack_base + self._stack_size - 8 if not self._safe_write_mem(ql, rsp, struct.pack(" None: try: ql.mem.map(addr, size, info="[cleonos-wine-stack]") return except TypeError: # Older qiling/unicorn builds may not support "info". pass except Exception: # Already mapped is acceptable. return try: ql.mem.map(addr, size) except Exception: # Already mapped is acceptable. pass def _on_entry_return(self, ql: Qiling, *_args) -> None: self.exit_code = self._read_reg(ql, "rax") ql.emu_stop() def _on_int80(self, ql: Qiling, *_args) -> None: syscall_id = self._read_reg(ql, "rax") arg0 = self._read_reg(ql, "rbx") arg1 = self._read_reg(ql, "rcx") arg2 = self._read_reg(ql, "rdx") self.state.context_switches = u64(self.state.context_switches + 1) ret = self._dispatch_syscall(ql, syscall_id, arg0, arg1, arg2) self._write_reg(ql, "rax", u64(ret)) def _dispatch_syscall(self, ql: Qiling, sid: int, arg0: int, arg1: int, arg2: int) -> int: if sid == SYS_LOG_WRITE: data = self._read_guest_bytes(ql, arg0, arg1) self._host_write(data.decode("utf-8", errors="replace")) return len(data) if sid == SYS_TIMER_TICKS: return self.state.timer_ticks() if sid == SYS_TASK_COUNT: return self.state.task_count if sid == SYS_CUR_TASK: return self.state.current_task if sid == SYS_SERVICE_COUNT: return self.state.service_count if sid == SYS_SERVICE_READY_COUNT: return self.state.service_ready if sid == SYS_CONTEXT_SWITCHES: return self.state.context_switches if sid == SYS_KELF_COUNT: return self.state.kelf_count if sid == SYS_KELF_RUNS: return self.state.kelf_runs if sid == SYS_FS_NODE_COUNT: return self._fs_node_count() if sid == SYS_FS_CHILD_COUNT: return self._fs_child_count(ql, arg0) if sid == SYS_FS_GET_CHILD_NAME: return self._fs_get_child_name(ql, arg0, arg1, arg2) if sid == SYS_FS_READ: return self._fs_read(ql, arg0, arg1, arg2) if sid == SYS_EXEC_PATH: return self._exec_path(ql, arg0) if sid == SYS_EXEC_REQUESTS: return self.state.exec_requests if sid == SYS_EXEC_SUCCESS: return self.state.exec_success if sid == SYS_USER_SHELL_READY: return self.state.user_shell_ready if sid == SYS_USER_EXEC_REQUESTED: return self.state.user_exec_requested if sid == SYS_USER_LAUNCH_TRIES: return self.state.user_launch_tries if sid == SYS_USER_LAUNCH_OK: return self.state.user_launch_ok if sid == SYS_USER_LAUNCH_FAIL: return self.state.user_launch_fail if sid == SYS_TTY_COUNT: return self.state.tty_count if sid == SYS_TTY_ACTIVE: return self.state.tty_active if sid == SYS_TTY_SWITCH: if arg0 >= self.state.tty_count: return u64_neg1() self.state.tty_active = int(arg0) return 0 if sid == SYS_TTY_WRITE: data = self._read_guest_bytes(ql, arg0, arg1) self._host_write(data.decode("utf-8", errors="replace")) return len(data) if sid == SYS_TTY_WRITE_CHAR: ch = chr(arg0 & 0xFF) if ch in ("\b", "\x7f"): self._host_write("\b \b") else: self._host_write(ch) return 0 if sid == SYS_KBD_GET_CHAR: key = self.state.pop_key() return u64_neg1() if key is None else key # Unknown syscall: keep app running, report failure. return u64_neg1() def _host_write(self, text: str) -> None: if not text: return sys.stdout.write(text) sys.stdout.flush() def _fs_node_count(self) -> int: count = 1 # root node for root, dirs, files in os.walk(self.rootfs): dirs[:] = [d for d in dirs if not d.startswith(".")] files = [f for f in files if not f.startswith(".")] count += len(dirs) + len(files) _ = root return count def _fs_child_count(self, ql: Qiling, dir_ptr: int) -> int: path = self._read_guest_cstring(ql, dir_ptr) host_dir = self._guest_to_host(path, must_exist=True) if host_dir is None or not host_dir.is_dir(): return u64_neg1() return len(self._list_children(host_dir)) def _fs_get_child_name(self, ql: Qiling, dir_ptr: int, index: int, out_ptr: int) -> int: if out_ptr == 0: return 0 path = self._read_guest_cstring(ql, dir_ptr) host_dir = self._guest_to_host(path, must_exist=True) if host_dir is None or not host_dir.is_dir(): return 0 children = self._list_children(host_dir) if index >= len(children): return 0 name = children[int(index)] encoded = name.encode("utf-8", errors="replace") if len(encoded) >= FS_NAME_MAX: encoded = encoded[: FS_NAME_MAX - 1] self._safe_write_mem(ql, out_ptr, encoded + b"\x00") return 1 def _fs_read(self, ql: Qiling, path_ptr: int, out_ptr: int, buf_size: int) -> int: if out_ptr == 0 or buf_size == 0: return 0 path = self._read_guest_cstring(ql, path_ptr) host_path = self._guest_to_host(path, must_exist=True) if host_path is None or not host_path.is_file(): return 0 read_size = int(min(buf_size, MAX_IO_READ)) try: data = host_path.read_bytes()[:read_size] except Exception: return 0 if not data: return 0 self._safe_write_mem(ql, out_ptr, data) return len(data) def _exec_path(self, ql: Qiling, path_ptr: int) -> int: path = self._read_guest_cstring(ql, path_ptr) guest_path = self._normalize_guest_path(path) host_path = self._guest_to_host(guest_path, must_exist=True) self.state.exec_requests = u64(self.state.exec_requests + 1) self.state.user_exec_requested = 1 self.state.user_launch_tries = u64(self.state.user_launch_tries + 1) if host_path is None or not host_path.is_file(): self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) return u64_neg1() if self.depth >= self.max_exec_depth: print(f"[WINE][WARN] exec depth exceeded: {guest_path}", file=sys.stderr) self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) return u64_neg1() child = CLeonOSWine( host_path, self.rootfs, guest_path_hint=guest_path, state=self.state, depth=self.depth + 1, max_exec_depth=self.max_exec_depth, no_kbd=True, verbose=self.verbose, top_level=False, ) child_ret = child.run() if child_ret is None: self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) return u64_neg1() self.state.exec_success = u64(self.state.exec_success + 1) self.state.user_launch_ok = u64(self.state.user_launch_ok + 1) if guest_path.lower().startswith("/system/"): self.state.kelf_runs = u64(self.state.kelf_runs + 1) return 0 def _guest_to_host(self, guest_path: str, *, must_exist: bool) -> Optional[Path]: norm = self._normalize_guest_path(guest_path) if norm == "/": return self.rootfs if (not must_exist or self.rootfs.exists()) else None current = self.rootfs for part in [p for p in norm.split("/") if p]: candidate = current / part if candidate.exists(): current = candidate continue if current.exists() and current.is_dir(): match = self._find_case_insensitive(current, part) if match is not None: current = match continue current = candidate if must_exist and not current.exists(): return None return current @staticmethod def _find_case_insensitive(parent: Path, name: str) -> Optional[Path]: target = name.lower() try: for entry in parent.iterdir(): if entry.name.lower() == target: return entry except Exception: return None return None @staticmethod def _normalize_guest_path(path: str) -> str: p = (path or "").replace("\\", "/").strip() if not p: return "/" if not p.startswith("/"): p = "/" + p parts = [] for token in p.split("/"): if token in ("", "."): continue if token == "..": if parts: parts.pop() continue parts.append(token) return "/" + "/".join(parts) @staticmethod def _list_children(dir_path: Path): try: names = [entry.name for entry in dir_path.iterdir() if not entry.name.startswith(".")] except Exception: return [] names.sort(key=lambda x: x.lower()) return names def _read_guest_cstring(self, ql: Qiling, addr: int, max_len: int = MAX_CSTR) -> str: if addr == 0: return "" out = bytearray() for i in range(max_len): try: chunk = ql.mem.read(addr + i, 1) except Exception: break if not chunk or chunk[0] == 0: break out.append(chunk[0]) return out.decode("utf-8", errors="replace") def _read_guest_bytes(self, ql: Qiling, addr: int, size: int) -> bytes: if addr == 0 or size == 0: return b"" safe_size = int(min(size, MAX_IO_READ)) try: return bytes(ql.mem.read(addr, safe_size)) except Exception: return b"" @staticmethod def _safe_write_mem(ql: Qiling, addr: int, data: bytes) -> bool: if addr == 0 or not data: return False try: ql.mem.write(addr, data) return True except Exception: return False @staticmethod def _read_reg(ql: Qiling, reg: str) -> int: arch_regs = getattr(getattr(ql, "arch", None), "regs", None) if arch_regs is not None: if hasattr(arch_regs, reg): return int(getattr(arch_regs, reg)) if hasattr(arch_regs, "read"): try: return int(arch_regs.read(reg)) except Exception: pass reg_obj = getattr(ql, "reg", None) if reg_obj is not None: if hasattr(reg_obj, reg): return int(getattr(reg_obj, reg)) if hasattr(reg_obj, "read"): try: return int(reg_obj.read(reg)) except Exception: pass raise RuntimeError(f"cannot read register: {reg}") @staticmethod def _write_reg(ql: Qiling, reg: str, value: int) -> None: v = u64(value) arch_regs = getattr(getattr(ql, "arch", None), "regs", None) if arch_regs is not None: if hasattr(arch_regs, reg): setattr(arch_regs, reg, v) return if hasattr(arch_regs, "write"): try: arch_regs.write(reg, v) return except Exception: pass reg_obj = getattr(ql, "reg", None) if reg_obj is not None: if hasattr(reg_obj, reg): setattr(reg_obj, reg, v) return if hasattr(reg_obj, "write"): try: reg_obj.write(reg, v) return except Exception: pass raise RuntimeError(f"cannot write register: {reg}") @staticmethod def _read_elf_entry(path: Path) -> int: try: data = path.read_bytes()[:64] except Exception: return 0 if len(data) < 64: return 0 if data[0:4] != b"\x7fELF": return 0 if data[4] != 2 or data[5] != 1: return 0 return struct.unpack_from(" Path: if path_arg: root = Path(path_arg).expanduser().resolve() if not root.exists() or not root.is_dir(): raise FileNotFoundError(f"rootfs not found: {root}") return root candidates = [ Path("build/x86_64/ramdisk_root"), Path("ramdisk"), ] for candidate in candidates: if candidate.exists() and candidate.is_dir(): return candidate.resolve() raise FileNotFoundError("rootfs not found; pass --rootfs") def _guest_to_host_for_resolve(rootfs: Path, guest_path: str) -> Optional[Path]: norm = CLeonOSWine._normalize_guest_path(guest_path) if norm == "/": return rootfs current = rootfs for part in [p for p in norm.split("/") if p]: candidate = current / part if candidate.exists(): current = candidate continue if current.exists() and current.is_dir(): match = None for entry in current.iterdir(): if entry.name.lower() == part.lower(): match = entry break if match is not None: current = match continue current = candidate if current.exists(): return current return None def resolve_elf_target(elf_arg: str, rootfs: Path) -> Tuple[Path, str]: host_candidate = Path(elf_arg).expanduser() if host_candidate.exists(): host_path = host_candidate.resolve() try: rel = host_path.relative_to(rootfs) guest_path = "/" + rel.as_posix() except ValueError: guest_path = "/" + host_path.name return host_path, guest_path # Fallback: treat as CLeonOS guest path. guest_path = CLeonOSWine._normalize_guest_path(elf_arg) host_path = _guest_to_host_for_resolve(rootfs, guest_path) if host_path is None: raise FileNotFoundError(f"ELF not found as host path or guest path: {elf_arg}") return host_path.resolve(), guest_path def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="CLeonOS-Wine: run CLeonOS ELF with Qiling.") parser.add_argument("elf", help="Target ELF path. Supports /guest/path or host file path.") parser.add_argument("--rootfs", help="Rootfs directory (default: build/x86_64/ramdisk_root).") parser.add_argument("--no-kbd", action="store_true", help="Disable host keyboard input pump.") parser.add_argument("--max-exec-depth", type=int, default=DEFAULT_MAX_EXEC_DEPTH, help="Nested exec depth guard.") parser.add_argument("--verbose", action="store_true", help="Enable verbose runner output.") return parser.parse_args() def main() -> int: args = parse_args() try: rootfs = resolve_rootfs(args.rootfs) elf_path, guest_path = resolve_elf_target(args.elf, rootfs) except Exception as exc: print(f"[WINE][ERROR] {exc}", file=sys.stderr) return 2 if args.verbose: print(f"[WINE] rootfs={rootfs}", file=sys.stderr) print(f"[WINE] elf={elf_path}", file=sys.stderr) print(f"[WINE] guest={guest_path}", file=sys.stderr) state = SharedKernelState() runner = CLeonOSWine( elf_path=elf_path, rootfs=rootfs, guest_path_hint=guest_path, state=state, max_exec_depth=max(1, args.max_exec_depth), no_kbd=args.no_kbd, verbose=args.verbose, top_level=True, ) ret = runner.run() if ret is None: return 1 if args.verbose: print(f"\n[WINE] exit=0x{ret:016X}", file=sys.stderr) return int(ret & 0xFF) if __name__ == "__main__": raise SystemExit(main())