|
1 | | -# Copyright (c) 2017 Linaro Limited. |
2 | | -# |
3 | | -# SPDX-License-Identifier: Apache-2.0 |
| 1 | +# scripts/west_commands/runners/qemu.py |
| 2 | +""" |
| 3 | +Minimal QEMU runner for Zephyr (starter). |
| 4 | +Drop this under scripts/west_commands/runners/ and extend as needed. |
| 5 | +""" |
4 | 6 |
|
5 | | -'''Runner stub for QEMU.''' |
| 7 | +import contextlib |
| 8 | +import os |
| 9 | +import shlex |
| 10 | +import shutil |
| 11 | +import subprocess |
| 12 | +import tempfile |
| 13 | +from pathlib import Path |
6 | 14 |
|
7 | | -from runners.core import RunnerCaps, ZephyrBinaryRunner |
| 15 | +from runners.core import RunnerConfig, ZephyrBinaryRunner, log |
8 | 16 |
|
9 | 17 |
|
10 | | -class QemuBinaryRunner(ZephyrBinaryRunner): |
11 | | - '''Place-holder for QEMU runner customizations.''' |
| 18 | +# Runner metadata |
| 19 | +class QemuRunner(ZephyrBinaryRunner): |
| 20 | + name = "qemu" |
| 21 | + description = "Run Zephyr ELF using QEMU (starter implementation)" |
12 | 22 |
|
13 | 23 | @classmethod |
14 | | - def name(cls): |
15 | | - return 'qemu' |
| 24 | + def add_parser(cls, parser): |
| 25 | + super().add_parser(parser) |
| 26 | + parser.add_argument( |
| 27 | + "--qemu-binary", |
| 28 | + help="Path to qemu-system-<arch> binary (if omitted, a best-guess is used)", |
| 29 | + ) |
| 30 | + parser.add_argument( |
| 31 | + "--qemu-arg", |
| 32 | + action="append", |
| 33 | + help="Extra CLI args to append to qemu (can be given multiple times)", |
| 34 | + ) |
| 35 | + parser.add_argument( |
| 36 | + "--qemu-serial", |
| 37 | + default=None, |
| 38 | + help="Serial backend: stdio, pty, file:<path> (default: stdio if available)", |
| 39 | + ) |
| 40 | + parser.add_argument( |
| 41 | + "--qemu-keep-fifos", |
| 42 | + action="store_true", |
| 43 | + help="Do not remove temporary FIFO/tty files on exit (for debugging)", |
| 44 | + ) |
16 | 45 |
|
17 | 46 | @classmethod |
18 | | - def capabilities(cls): |
19 | | - # This is a stub. |
20 | | - return RunnerCaps(commands=set()) |
| 47 | + def create(cls, cfg: RunnerConfig): |
| 48 | + """ |
| 49 | + Factory called by west. Return QemuRunner(cfg) if the runner can run in this environment, |
| 50 | + or None otherwise. |
| 51 | + """ |
| 52 | + # Only create runner if we have an ELF |
| 53 | + if not cfg.elf_file: |
| 54 | + return None |
21 | 55 |
|
22 | | - @classmethod |
23 | | - def do_add_parser(cls, parser): |
24 | | - pass # Nothing to do. |
| 56 | + # Basic availability check is deferred until do_run; allow create to |
| 57 | + # succeed so 'west flash --context' lists it. |
| 58 | + return QemuRunner(cfg) |
25 | 59 |
|
26 | | - @classmethod |
27 | | - def do_create(cls, cfg, args): |
28 | | - return QemuBinaryRunner(cfg) |
| 60 | + def __init__(self, cfg: RunnerConfig): |
| 61 | + super().__init__(cfg) |
| 62 | + self.cfg = cfg |
| 63 | + self.elf = cfg.elf_file |
| 64 | + self.build_dir = cfg.build_dir or os.getcwd() |
| 65 | + self._tmpdir = None |
| 66 | + self._fifos = [] |
| 67 | + |
| 68 | + def do_run(self, command, timeout=0, **kwargs): |
| 69 | + """ |
| 70 | + Entry point invoked when user runs "west flash/run -r qemu" (or similar). |
| 71 | + This should prepare FIFOs/serial, build command line, and spawn QEMU. |
| 72 | + """ |
| 73 | + # 1) Ensure qemu binary |
| 74 | + qemu_bin = kwargs.get("qemu_binary") or self._guess_qemu_binary() |
| 75 | + if not qemu_bin or not shutil.which(qemu_bin): |
| 76 | + raise RuntimeError( |
| 77 | + "QEMU binary not found: set --qemu-binary or install qemu-system-* on PATH" |
| 78 | + ) |
| 79 | + |
| 80 | + # 2) Prepare temporary directory for FIFOs/ptys |
| 81 | + self._tmpdir = Path(tempfile.mkdtemp(prefix="zephyr-qemu-")) |
| 82 | + log.debug("Using temporary qemu dir: %s", str(self._tmpdir)) |
| 83 | + |
| 84 | + # 3) Prepare serial backend (simple: use stdio or create FIFO for outside tools) |
| 85 | + serial_spec = kwargs.get("qemu_serial") or "stdio" |
| 86 | + serial_option = self._prepare_serial(serial_spec) |
| 87 | + |
| 88 | + # 4) Build qemu commandline |
| 89 | + qemu_cmd = [qemu_bin] |
| 90 | + qemu_cmd += self._platform_defaults() |
| 91 | + qemu_cmd += ["-kernel", str(self.elf)] |
| 92 | + qemu_cmd += serial_option |
| 93 | + extra_args = kwargs.get("qemu_arg") or [] |
| 94 | + # allow both list or single-string extra args |
| 95 | + if isinstance(extra_args, str): |
| 96 | + extra_args = shlex.split(extra_args) |
| 97 | + for arg in extra_args: |
| 98 | + if isinstance(arg, str): |
| 99 | + qemu_cmd += shlex.split(arg) |
| 100 | + |
| 101 | + # Logging / dry-run support: |
| 102 | + log.inf("QEMU cmd: %s", " ".join(shlex.quote(x) for x in qemu_cmd)) |
| 103 | + |
| 104 | + # 5) Spawn QEMU process |
| 105 | + proc = subprocess.Popen(qemu_cmd, cwd=self.build_dir) |
| 106 | + |
| 107 | + try: |
| 108 | + # Wait: if timeout==0 then wait forever until QEMU exits |
| 109 | + ret = proc.wait(timeout=None if timeout == 0 else timeout) |
| 110 | + return ret |
| 111 | + finally: |
| 112 | + # 6) Cleanup |
| 113 | + if not kwargs.get("qemu_keep_fifos"): |
| 114 | + self._cleanup() |
| 115 | + |
| 116 | + def _guess_qemu_binary(self): |
| 117 | + # Default fallback |
| 118 | + for p in ( |
| 119 | + "qemu-system-x86_64", |
| 120 | + "qemu-system-x86", |
| 121 | + "qemu-system-arm", |
| 122 | + "qemu-system-riscv64", |
| 123 | + ): |
| 124 | + if shutil.which(p): |
| 125 | + return p |
| 126 | + return None |
| 127 | + |
| 128 | + def _platform_defaults(self): |
| 129 | + # Minimal defaults. Extend per-board: memory, machine type etc. |
| 130 | + # For common x86 qemu_x86: use -M pc -m 512M -nographic |
| 131 | + board = (self.cfg.board or "").lower() if hasattr(self.cfg, "board") else "" |
| 132 | + if "qemu_x86" in board or "qemu_x86" in str(self.cfg.board_dir): |
| 133 | + return ["-M", "pc", "-m", "512", "-nographic"] |
| 134 | + # cortex-m3 qemu example: |
| 135 | + if "cortex_m3" in board or "qemu_cortex_m3" in str(self.cfg.board_dir): |
| 136 | + return ["-M", "lm3s6965evb", "-nographic"] |
| 137 | + # fall back to minimal no-graphic |
| 138 | + return ["-nographic"] |
| 139 | + |
| 140 | + def _prepare_serial(self, spec): |
| 141 | + """ |
| 142 | + Return list of qemu args for serial based on spec. |
| 143 | + Spec examples: |
| 144 | + - "stdio": returns ["-serial", "stdio"] |
| 145 | + - "file:/tmp/qemu-serial": creates file, returns ["-serial", "file:..."] |
| 146 | + - "fifo": creates fifo in tmpdir, returns ["-serial", "pipe:..."] |
| 147 | + """ |
| 148 | + if spec == "stdio": |
| 149 | + return ["-serial", "stdio"] |
| 150 | + if spec.startswith("file:"): |
| 151 | + path = spec.split(":", 1)[1] |
| 152 | + path = os.path.expanduser(path) |
| 153 | + # ensure containing dir exists |
| 154 | + Path(path).parent.mkdir(parents=True, exist_ok=True) |
| 155 | + return ["-serial", f"file:{path}"] |
| 156 | + if spec == "fifo" or spec.startswith("fifo:"): |
| 157 | + # Make two FIFOs for serial in/out (POSIX) |
| 158 | + in_fifo = str(self._tmpdir / "qemu-serial-in") |
| 159 | + out_fifo = str(self._tmpdir / "qemu-serial-out") |
| 160 | + try: |
| 161 | + os.mkfifo(in_fifo) |
| 162 | + os.mkfifo(out_fifo) |
| 163 | + self._fifos += [in_fifo, out_fifo] |
| 164 | + # Example approach: use tty server or pty trick -- keep this simple for now |
| 165 | + # QEMU supports -serial pipe:<name> on some configurations; |
| 166 | + # here we return one FIFO file for debugging. |
| 167 | + return ["-serial", f"file:{out_fifo}"] |
| 168 | + except Exception as e: |
| 169 | + log.err("Failed creating FIFOs: %s", e) |
| 170 | + return ["-serial", "stdio"] |
| 171 | + # fallback |
| 172 | + return ["-serial", "stdio"] |
29 | 173 |
|
30 | | - def do_run(self, command, **kwargs): |
31 | | - pass |
| 174 | + def _cleanup(self): |
| 175 | + # Remove FIFOs and tmpdir |
| 176 | + for p in self._fifos: |
| 177 | + with contextlib.suppress(Exception): |
| 178 | + os.remove(p) |
| 179 | + if self._tmpdir: |
| 180 | + with contextlib.suppress(Exception): |
| 181 | + shutil.rmtree(self._tmpdir) |
0 commit comments