|
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 |
4 | 10 |
|
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 |
6 | 19 |
|
7 | | -from runners.core import RunnerCaps, ZephyrBinaryRunner |
| 20 | +from runners.core import ZephyrBinaryRunner, RunnerConfig, RunnerCaps, log |
8 | 21 |
|
9 | 22 |
|
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)" |
12 | 26 |
|
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) |
16 | 30 |
|
17 | 31 | @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") |
21 | 40 |
|
22 | 41 | @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) |
25 | 48 |
|
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"] |
29 | 170 |
|
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