Skip to content

Commit a250082

Browse files
authored
Merge pull request #184 from lool/run-qemu
run-qemu.py
2 parents 1b72a72 + 39b88a4 commit a250082

File tree

2 files changed

+318
-60
lines changed

2 files changed

+318
-60
lines changed

README.md

Lines changed: 29 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -182,75 +182,44 @@ NB: It's also possible to run qdl from the host while the baord is not connected
182182

183183
Want to join in the development? Changes welcome! See [CONTRIBUTING.md file](CONTRIBUTING.md) for step by step instructions.
184184

185-
### Test an image locally with qemu
185+
### Boot an image locally with QEMU (helper script)
186186

187-
You can boot an image locally with qemu as follows:
187+
Use the `scripts/run-qemu.py` helper to boot generated disk images under QEMU. It automatically:
188+
- Detects your OS and locates an aarch64 UEFI firmware (Debian/Ubuntu: qemu-efi-aarch64; macOS Homebrew: edk2-aarch64).
189+
- Presents the image via SCSI with the correct sector size (4096 for UFS, 512 for SD/eMMC).
190+
- Creates a temporary qcow2 copy-on-write overlay by default (your base image remains unchanged).
191+
- Provides GUI display by default (Gtk on Linux, Cocoa on macOS) and headless mode with `--headless`.
188192

189-
1. Install dependencies. `qemu-system-arm` is required together with
190-
an aarch64 build of UEFI. On Debian and Ubuntu, this is provided by the
191-
`qemu-system-arm` package which recommends `qemu-efi-aarch64`:
192-
```bash
193-
sudo apt install qemu-system-arm qemu-efi-aarch64
194-
```
195-
196-
1. As above under "Usage", build the disk image from the root filesystem
197-
tarball if you haven't done this already:
198-
```bash
199-
debos debos-recipes/qualcomm-linux-debian-image.yaml
200-
```
201-
202-
1. Run qemu as follows:
203-
```bash
204-
# SCSI is required to present a device with a matching 4096 sector size
205-
# inside the VM
206-
qemu-system-aarch64 -cpu cortex-a57 -m 2048 -M virt -nographic \
207-
-device virtio-scsi-pci,id=scsi1 \
208-
-device scsi-hd,bus=scsi1.0,drive=disk1,physical_block_size=4096,logical_block_size=4096 \
209-
-drive if=none,file=disk-ufs.img,format=raw,id=disk1 \
210-
-bios /usr/share/AAVMF/AAVMF_CODE.fd
211-
```
212-
213-
#### Copy on write
193+
Dependencies:
194+
- Debian/Ubuntu: `sudo apt install qemu-efi-aarch64 qemu-system-arm qemu-utils`
195+
- macOS (Homebrew): `brew install qemu`
214196

215-
Instead of modifying `file-ufs.img`, you can arrange copy-on-write, for example
216-
to reproduce the same first boot multiple times:
197+
Basic usage:
198+
```bash
199+
# Auto-detects disk-ufs.img or disk-sdcard.img in the current directory
200+
scripts/run-qemu.py
217201
218-
1. Prepare a qcow file to contain the writes, backed by `disk-ufs.img`:
219-
```bash
220-
qemu-img create -b disk-ufs.img -f qcow -F raw disk1.qcow
221-
```
202+
# Explicit storage type (sector size set accordingly)
203+
scripts/run-qemu.py --storage ufs
204+
scripts/run-qemu.py --storage sdcard
222205
223-
1. Run qemu as follows:
224-
```bash
225-
qemu-system-aarch64 -cpu cortex-a57 -m 2048 -M virt -nographic \
226-
-device virtio-scsi-pci,id=scsi1 \
227-
-device scsi-hd,bus=scsi1.0,drive=disk1,physical_block_size=4096,logical_block_size=4096 \
228-
-drive if=none,file=disk1.img,format=qcow,id=disk1 \
229-
-bios /usr/share/AAVMF/AAVMF_CODE.fd
230-
```
206+
# Use a specific image path
207+
scripts/run-qemu.py --image /path/to/disk-ufs.img
231208
232-
#### Direct kernel boot
209+
# Run headless (no GUI), with serial console on stdio
210+
scripts/run-qemu.py --headless
233211
234-
For debugging purposes, it is sometimes useful to boot the kernel directly, for
235-
example to confirm that an issue in the image lies in the bootloader
236-
installation. You can do this as follows:
212+
# Disable the COW overlay to persist changes to the image
213+
scripts/run-qemu.py --no-cow
237214
238-
1. Extract the rootfs:
239-
```bash
240-
mkdir rootfs
241-
tar xzC rootfs -f rootfs.tar.gz
242-
```
215+
# Pass extra QEMU arguments (example: 4 vCPUs and 4 GiB RAM)
216+
scripts/run-qemu.py --qemu-args "-smp 4 -m 4096"
217+
```
243218

244-
2. Run qemu against the kernel and initrd present inside the rootfs directly:
245-
```bash
246-
qemu-system-aarch64 -cpu cortex-a57 -m 2048 -M virt -nographic \
247-
-device virtio-scsi-pci,id=scsi1 \
248-
-device scsi-hd,bus=scsi1.0,drive=disk1,physical_block_size=4096,logical_block_size=4096 \
249-
-drive if=none,file=disk-ufs.img,format=raw,id=disk1 \
250-
-kernel rootfs/boot/vmlinuz-*
251-
-initrd rootfs/boot/initrd.img-*
252-
-append root=/dev/sda2
253-
```
219+
Notes:
220+
- If neither `disk-ufs.img` nor `disk-sdcard.img` is found and `--image` is not provided, the script will exit with an error.
221+
- On Linux, the script looks for `/usr/share/qemu-efi-aarch64/QEMU_EFI.fd`. On macOS with Homebrew, it uses `share/qemu/edk2-aarch64-code.fd` from the `qemu` formula.
222+
- The overlay is cleaned up automatically when QEMU exits. Use `--no-cow` to make changes persistent on the base image.
254223

255224
## Reporting Issues
256225

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)