Skip to content

Commit 7ef3765

Browse files
committed
runners: improve QEMU runner (RunnerCaps, safer serial/FIFO handling, extra-args)
1 parent 91b1b84 commit 7ef3765

File tree

1 file changed

+174
-20
lines changed
  • scripts/west_commands/runners

1 file changed

+174
-20
lines changed
Lines changed: 174 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,185 @@
1-
# Copyright (c) 2017 Linaro Limited.
2-
#
3-
# SPDX-License-Identifier: Apache-2.0
1+
# scripts/west_commands/runners/qemu.py
2+
"""
3+
Improved QEMU runner for Zephyr (PR-ready starter).
4+
- Provides RunnerCaps for run/flash/debug where applicable
5+
- Supports extra CLI args and qemu-binary override
6+
- Safer handling of temp dir, FIFOs, and PTY fallback
7+
- Board heuristics that can be extended via runners.yaml
8+
"""
9+
from __future__ import annotations
410

5-
'''Runner stub for QEMU.'''
11+
import os
12+
import shlex
13+
import shutil
14+
import subprocess
15+
import tempfile
16+
import time
17+
from pathlib import Path
18+
from typing import List, Optional
619

7-
from runners.core import RunnerCaps, ZephyrBinaryRunner
20+
from runners.core import ZephyrBinaryRunner, RunnerConfig, RunnerCaps, log
821

922

10-
class QemuBinaryRunner(ZephyrBinaryRunner):
11-
'''Place-holder for QEMU runner customizations.'''
23+
class QemuRunner(ZephyrBinaryRunner):
24+
name = "qemu"
25+
description = "Run Zephyr ELF using QEMU (improved starter runner)"
1226

13-
@classmethod
14-
def name(cls):
15-
return 'qemu'
27+
# Capabilities: this runner supports 'run' (execute ELF in qemu).
28+
# Flash/debug can be added as needed.
29+
CAPABILITIES = RunnerCaps(run=True)
1630

1731
@classmethod
18-
def capabilities(cls):
19-
# This is a stub.
20-
return RunnerCaps(commands=set())
32+
def add_parser(cls, parser):
33+
super().add_parser(parser)
34+
parser.add_argument("--qemu-binary", help="Path to qemu-system-<arch> binary")
35+
parser.add_argument("--qemu-arg", action="append", help="Extra CLI args to append to QEMU")
36+
parser.add_argument("--qemu-serial", default="stdio",
37+
help="Serial backend: stdio (default), file:/path, fifo, pty")
38+
parser.add_argument("--qemu-keep-tmp", action="store_true",
39+
help="Keep temporary files/dirs for debugging")
2140

2241
@classmethod
23-
def do_add_parser(cls, parser):
24-
pass # Nothing to do.
42+
def create(cls, cfg: RunnerConfig):
43+
# Only create runner if an ELF exists and a qemu-capable board is selected.
44+
if not cfg or not cfg.elf_file:
45+
return None
46+
# Optionally, check cfg.board metadata for qemu_ prefix; be permissive for now.
47+
return QemuRunner(cfg)
2548

26-
@classmethod
27-
def do_create(cls, cfg, args):
28-
return QemuBinaryRunner(cfg)
49+
def __init__(self, cfg: RunnerConfig):
50+
super().__init__(cfg)
51+
self.cfg = cfg
52+
self.elf = cfg.elf_file
53+
self.build_dir = Path(cfg.build_dir) if cfg.build_dir else Path.cwd()
54+
self._tmpdir: Optional[Path] = None
55+
self._created: List[Path] = []
56+
57+
def do_run(self, command: List[str], timeout: int = 0, **kwargs) -> int:
58+
# Locate qemu binary
59+
qemu_bin = kwargs.get("qemu_binary") or self._guess_qemu_binary()
60+
if not qemu_bin or not shutil.which(qemu_bin):
61+
raise RuntimeError("QEMU binary not found. Install qemu-system-* or pass --qemu-binary")
62+
63+
# Create temporary directory for FIFOs/PTY files
64+
self._tmpdir = Path(tempfile.mkdtemp(prefix="zephyr-qemu-"))
65+
log.debug("QEMU tmpdir: %s", str(self._tmpdir))
66+
67+
# Prepare serial/backend args
68+
serial_spec = kwargs.get("qemu_serial") or "stdio"
69+
serial_args = self._prepare_serial(serial_spec)
70+
71+
# Build base qemu command
72+
qemu_cmd = [qemu_bin]
73+
qemu_cmd += self._platform_flags()
74+
qemu_cmd += ["-kernel", str(self.elf)]
75+
qemu_cmd += serial_args
76+
77+
# Add extra args passed via --qemu-arg (list)
78+
extra_args = kwargs.get("qemu_arg") or []
79+
if isinstance(extra_args, str):
80+
extra_args = shlex.split(extra_args)
81+
for a in extra_args:
82+
if isinstance(a, str):
83+
qemu_cmd += shlex.split(a)
84+
85+
# Print diagnostic
86+
log.inf("Running QEMU: %s", " ".join(shlex.quote(x) for x in qemu_cmd))
87+
88+
proc = subprocess.Popen(qemu_cmd, cwd=str(self.build_dir))
89+
try:
90+
# Wait (timeout==0 implies wait forever)
91+
ret = proc.wait(timeout=None if timeout == 0 else timeout)
92+
return ret
93+
finally:
94+
if not kwargs.get("qemu_keep_tmp"):
95+
self._cleanup()
96+
97+
# Helper methods -------------------------------------------------
98+
99+
def _guess_qemu_binary(self) -> Optional[str]:
100+
# Heuristic order: prefer 64-bit x86 qemu for qemu_x86 boards
101+
candidates = ("qemu-system-x86_64", "qemu-system-x86", "qemu-system-riscv64",
102+
"qemu-system-arm", "qemu-system-aarch64")
103+
for name in candidates:
104+
if shutil.which(name):
105+
return name
106+
return None
107+
108+
def _platform_flags(self) -> List[str]:
109+
"""
110+
Return a small set of default machine/memory flags depending on board name.
111+
Extend this to parse runners.yaml or board metadata as needed.
112+
"""
113+
board = (self.cfg.board or "").lower() if hasattr(self.cfg, "board") else ""
114+
board_dir = str(self.cfg.board_dir) if hasattr(self.cfg, "board_dir") else ""
115+
116+
if "qemu_x86" in board or "qemu_x86" in board_dir:
117+
# qemu_x86 default used in Zephyr's samples; these match the qemu.cmake defaults
118+
return ["-M", "q35", "-m", "32", "-cpu", "qemu32,+nx,+pae", "-no-reboot", "-nographic",
119+
"-machine", "acpi=off", "-net", "none", "-icount", "shift=5,align=off,sleep=off", "-rtc", "clock=vm"]
120+
# cortex-m defaults (example)
121+
if "qemu_cortex_m" in board or "lm3s" in board_dir:
122+
return ["-M", "lm3s6965evb", "-nographic"]
123+
# fallback minimal
124+
return ["-nographic"]
125+
126+
def _prepare_serial(self, spec: str) -> List[str]:
127+
"""
128+
Build qemu serial args based on 'spec' (stdio, file:/path, fifo, pty).
129+
Prefer chardev+serial+mon setup consistent with qemu.cmake defaults.
130+
"""
131+
if spec == "stdio":
132+
return ["-chardev", "stdio,id=con,mux=on", "-serial", "chardev:con", "-mon", "chardev=con,mode=readline"]
133+
if spec.startswith("file:"):
134+
path = spec.split(":", 1)[1]
135+
path = os.path.expanduser(path)
136+
Path(path).parent.mkdir(parents=True, exist_ok=True)
137+
return ["-serial", f"file:{path}"]
138+
if spec == "fifo" or spec.startswith("fifo:"):
139+
return self._create_fifos()
140+
if spec == "pty":
141+
# QEMU's pty support on Linux usually creates a PTY automatically with -serial pty
142+
return ["-serial", "pty"]
143+
# default fallback
144+
return ["-chardev", "stdio,id=con,mux=on", "-serial", "chardev:con", "-mon", "chardev=con,mode=readline"]
145+
146+
def _create_fifos(self) -> List[str]:
147+
"""
148+
Create two FIFOs under the tmpdir. Returns a qemu -serial option referencing the output FIFO
149+
(for simple log capture). This function intentionally uses file: because pipe: syntax differs.
150+
"""
151+
if not self._tmpdir:
152+
self._tmpdir = Path(tempfile.mkdtemp(prefix="zephyr-qemu-"))
153+
in_fifo = self._tmpdir / "qemu-serial-in"
154+
out_fifo = self._tmpdir / "qemu-serial-out"
155+
try:
156+
# Remove if stale, then create
157+
for p in (in_fifo, out_fifo):
158+
try:
159+
if p.exists():
160+
p.unlink()
161+
except Exception:
162+
pass
163+
os.mkfifo(str(p))
164+
self._created.append(p)
165+
# Return a qemu arg that writes serial output to a file FIFO (readable by user/tools)
166+
return ["-serial", f"file:{str(out_fifo)}"]
167+
except Exception as e:
168+
log.err("Failed to create FIFOs (%s), falling back to stdio", e)
169+
return ["-chardev", "stdio,id=con,mux=on", "-serial", "chardev:con", "-mon", "chardev=con,mode=readline"]
29170

30-
def do_run(self, command, **kwargs):
31-
pass
171+
def _cleanup(self):
172+
# remove created files and tempdir
173+
for p in self._created:
174+
try:
175+
if p.exists():
176+
p.unlink()
177+
except Exception:
178+
pass
179+
if self._tmpdir:
180+
try:
181+
shutil.rmtree(self._tmpdir)
182+
except Exception:
183+
pass
184+
self._tmpdir = None
185+
self._created = []

0 commit comments

Comments
 (0)