Files
cleonos/wine/__tmp_raw.txt

945 lines
30 KiB
Python

#!/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("<Q", data, 0x20)[0]
phentsize = struct.unpack_from("<H", data, 0x36)[0]
phnum = struct.unpack_from("<H", data, 0x38)[0]
if phentsize == 0 or phnum == 0:
raise RuntimeError(f"ELF program headers missing: {path}")
loaded = 0
for i in range(phnum):
off = phoff + i * phentsize
if off + 56 > len(data):
break
p_type, p_flags, p_offset, p_vaddr, _p_paddr, p_filesz, p_memsz, _p_align = struct.unpack_from(
"<IIQQQQQQ", data, off
)
if p_type != 1 or p_memsz == 0:
continue
map_start = int(p_vaddr & ~0xFFF)
map_end = int((p_vaddr + p_memsz + 0xFFF) & ~0xFFF)
map_size = map_end - map_start
if map_size <= 0:
continue
self._map_memory(ql, map_start, map_size)
if p_filesz > 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("<Q", self._ret_sentinel)):
raise RuntimeError("failed to initialize custom stack")
self._write_reg(ql, "rsp", rsp)
self._write_reg(ql, "rbp", rsp)
self._write_reg(ql, "rip", self.entry)
def _map_memory(self, ql: Qiling, addr: int, size: int) -> 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("<Q", data, 0x18)[0]
def resolve_rootfs(path_arg: Optional[str]) -> 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())