Skip to content

Commit 1320a17

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 e6d5312 commit 1320a17

File tree

1 file changed

+292
-0
lines changed

1 file changed

+292
-0
lines changed

scripts/run-qemu.py

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

0 commit comments

Comments
 (0)