Skip to content

Commit f3ff670

Browse files
committed
feat(scripts): Add run-qemu helper
Add a script to help with running QEMU: - detects the EDK2 BIOS file on Linux and macOS - looks for default filenames - detects sector size - (optionally) creates a COW disk - emulates the lowest common denominator of supported boards - passes all relevant devices to get to a GUI - detects display backend - offers headless mode Fixes: #66 Signed-off-by: Loïc Minier <[email protected]>
1 parent 1b72a72 commit f3ff670

File tree

1 file changed

+289
-0
lines changed

1 file changed

+289
-0
lines changed

scripts/run-qemu.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
"""
5+
Run Debian disk images of various sector sizes with QEMU and an optional
6+
COW overlay.
7+
8+
Usage:
9+
- Set storage type, detect image file:
10+
run-qemu.py --storage ufs
11+
run-qemu.py --storage sdcard
12+
13+
- Set image file, detect storage type:
14+
run-qemu.py --image /path/to/disk-ufs.img
15+
16+
- Disable COW overlay (write to disk image):
17+
run-qemu.py --no-cow
18+
"""
19+
20+
import argparse
21+
import os
22+
import sys
23+
import shutil
24+
import subprocess
25+
import tempfile
26+
import platform
27+
import shlex
28+
from typing import Optional
29+
30+
DEFAULT_UFS_IMAGE = "disk-ufs.img"
31+
DEFAULT_SDCARD_IMAGE = "disk-sdcard.img"
32+
33+
34+
def find_bios_path() -> Optional[str]:
35+
"""
36+
Get OS specific aarch64 UEFI firmware path
37+
"""
38+
system = platform.system()
39+
candidates = []
40+
41+
if system == "Linux":
42+
# provided by qemu-efi-aarch64 in Debian bookwork/trixie/forky and in
43+
# Ubuntu jammy/noble/questing (as of writing)
44+
candidates.append("/usr/share/qemu-efi-aarch64/QEMU_EFI.fd")
45+
elif system == "Darwin":
46+
# check if brew is installed and get the prefix of the qemu recipe if
47+
# that recipe is installed
48+
brew = shutil.which("brew")
49+
if brew:
50+
try:
51+
completed = subprocess.run(
52+
[brew, "--prefix", "qemu"],
53+
stdout=subprocess.PIPE,
54+
stderr=subprocess.DEVNULL,
55+
text=True,
56+
)
57+
prefix = completed.stdout.strip()
58+
if prefix:
59+
# provided by qemu Homebrew recipe as of 10.1.1
60+
candidates.append(
61+
os.path.join(prefix, "share/qemu/edk2-aarch64-code.fd")
62+
)
63+
except Exception:
64+
pass
65+
else:
66+
sys.stderr.write(f"Unknown system {system}, patches welcome!\n")
67+
sys.exit(2)
68+
69+
for path in candidates:
70+
if os.path.exists(path):
71+
return path
72+
return None
73+
74+
75+
def main():
76+
parser = argparse.ArgumentParser(
77+
description=(
78+
"Run Debian disk images of various sector sizes with QEMU and an "
79+
"optional COW overlay."
80+
)
81+
)
82+
parser.add_argument(
83+
"--image",
84+
help=(
85+
"Path to the base disk image (.img). Default is to auto-detect "
86+
"disk-ufs.img or disk-sdcard.img."
87+
),
88+
)
89+
parser.add_argument(
90+
"--storage",
91+
choices=["ufs", "sdcard"],
92+
help=(
93+
"Storage type. If --image isn't provided, uses default file for "
94+
"the storage type."
95+
),
96+
)
97+
parser.add_argument(
98+
"--no-cow",
99+
action="store_true",
100+
help=(
101+
"Disable COW overlay. Without the overlay, the disk image will "
102+
"be modified."
103+
),
104+
)
105+
parser.add_argument(
106+
"--headless",
107+
action="store_true",
108+
help="Run without GUI; sets -display none and -serial mon:stdio.",
109+
)
110+
parser.add_argument(
111+
"--qemu-args",
112+
dest="qemu_args",
113+
help=(
114+
"Extra arguments to pass to QEMU, e.g. "
115+
"'--qemu-args \"-smp 4 -m 4096\"'."
116+
),
117+
)
118+
args = parser.parse_args()
119+
120+
# OS; "Linux" on Debian/Ubuntu, and "Darwin" on macOS; used to detect
121+
# defaults
122+
system = platform.system()
123+
124+
bios_path = find_bios_path()
125+
126+
if not (
127+
shutil.which("qemu-system-aarch64")
128+
and shutil.which("qemu-img")
129+
and bios_path
130+
):
131+
sys.stderr.write("Missing qemu components.\n")
132+
system = platform.system()
133+
if system == "Darwin":
134+
sys.stderr.write(
135+
"With Homebrew, install via:\n"
136+
" brew install qemu\n"
137+
)
138+
elif system == "Linux":
139+
sys.stderr.write(
140+
"On Linux systems with apt, install via:\n"
141+
" apt install qemu-efi-aarch64 qemu-system-arm "
142+
"qemu-utils\n"
143+
)
144+
else:
145+
sys.stderr.write(f"Unknown system {system}, patches welcome!\n")
146+
sys.exit(1)
147+
148+
# determine image path and sector size
149+
if args.image:
150+
image_path = args.image
151+
if not os.path.exists(image_path):
152+
sys.stderr.write(f"Image not found: {image_path}\n")
153+
sys.exit(2)
154+
# if storage type was set, use it to set sector size; otherwise infer
155+
# from filename
156+
if args.storage == "ufs":
157+
sector_size = 4096
158+
elif args.storage == "sdcard":
159+
sector_size = 512
160+
else:
161+
# infer from filename
162+
fname = os.path.basename(image_path).lower()
163+
if "-ufs" in fname:
164+
sector_size = 4096
165+
elif "-sdcard" in fname or "-emmc" in fname:
166+
sector_size = 512
167+
else:
168+
# default to 4K unless specified
169+
sector_size = 4096
170+
else:
171+
if args.storage == "ufs":
172+
if not os.path.exists(DEFAULT_UFS_IMAGE):
173+
sys.stderr.write(
174+
f"Requested storage 'ufs' but {DEFAULT_UFS_IMAGE} not "
175+
"found. Please provide --image path.\n"
176+
)
177+
sys.exit(2)
178+
image_path = DEFAULT_UFS_IMAGE
179+
sector_size = 4096
180+
elif args.storage == "sdcard":
181+
if not os.path.exists(DEFAULT_SDCARD_IMAGE):
182+
sys.stderr.write(
183+
f"Requested storage 'sdcard' but {DEFAULT_SDCARD_IMAGE} "
184+
"not found. Please provide --image path.\n"
185+
)
186+
sys.exit(2)
187+
image_path = DEFAULT_SDCARD_IMAGE
188+
sector_size = 512
189+
else:
190+
# storage type not set, look for default file names
191+
if os.path.exists(DEFAULT_UFS_IMAGE):
192+
image_path = DEFAULT_UFS_IMAGE
193+
sector_size = 4096
194+
elif os.path.exists(DEFAULT_SDCARD_IMAGE):
195+
image_path = DEFAULT_SDCARD_IMAGE
196+
sector_size = 512
197+
else:
198+
sys.stderr.write(
199+
f"Neither {DEFAULT_UFS_IMAGE} nor {DEFAULT_SDCARD_IMAGE} "
200+
"found. Please provide --image path.\n"
201+
)
202+
sys.exit(2)
203+
204+
# default to Gtk+ GUI, except on macOS where Cocoa is preferred
205+
display_backend = "gtk"
206+
if system == "Darwin":
207+
display_backend = "cocoa"
208+
if args.headless:
209+
display_backend = "none"
210+
211+
with tempfile.TemporaryDirectory(prefix="qemu-cow-") as temp_dir:
212+
# default to using the image as drive
213+
drive_file = image_path
214+
drive_format = "raw"
215+
216+
# create and use COW overlay unless disabled
217+
if not args.no_cow:
218+
overlay_path = os.path.join(temp_dir, "overlay.qcow")
219+
try:
220+
cmd = [
221+
"qemu-img",
222+
"create",
223+
"-b",
224+
os.path.abspath(image_path),
225+
"-f",
226+
"qcow2",
227+
"-F",
228+
"raw",
229+
overlay_path,
230+
]
231+
print("Running:", " ".join(cmd))
232+
subprocess.run(cmd, check=True)
233+
except subprocess.CalledProcessError as e:
234+
sys.stderr.write(f"Failed to create COW overlay: {e}\n")
235+
sys.exit(1)
236+
drive_file = overlay_path
237+
drive_format = "qcow2"
238+
239+
# run QEMU
240+
cmd = [
241+
"qemu-system-aarch64",
242+
# oldest supported CPU
243+
"-cpu",
244+
"cortex-a57",
245+
# smallest memory size in all supported platforms
246+
"-m",
247+
"2048",
248+
# performant and complete model
249+
"-M",
250+
"virt",
251+
"-device",
252+
"virtio-gpu-pci",
253+
"-display",
254+
display_backend,
255+
"-device",
256+
"usb-ehci,id=ehci",
257+
"-device",
258+
"usb-kbd",
259+
"-device",
260+
"usb-mouse",
261+
"-device",
262+
"virtio-scsi-pci,id=scsi1",
263+
"-device",
264+
f"scsi-hd,bus=scsi1.0,drive=disk1,"
265+
f"physical_block_size={sector_size},"
266+
f"logical_block_size={sector_size}",
267+
"-drive",
268+
f"if=none,file={drive_file},format={drive_format},"
269+
f"id=disk1",
270+
"-bios",
271+
bios_path,
272+
]
273+
274+
if args.headless:
275+
cmd.extend(["-serial", "mon:stdio"])
276+
277+
if args.qemu_args:
278+
cmd.extend(shlex.split(args.qemu_args))
279+
280+
print("Running:", " ".join(cmd))
281+
try:
282+
subprocess.run(cmd, check=True)
283+
except subprocess.CalledProcessError as e:
284+
sys.stderr.write(f"QEMU exited with error: {e}\n")
285+
sys.exit(e.returncode)
286+
287+
288+
if __name__ == "__main__":
289+
main()

0 commit comments

Comments
 (0)