Skip to content

Commit 17f4157

Browse files
committed
boot-qemu.py: Initial rewrite
This only supports x86 and x86_64 only, other architectures will be added with time. Signed-off-by: Nathan Chancellor <nathan@kernel.org>
1 parent 0fdee4e commit 17f4157

1 file changed

Lines changed: 247 additions & 0 deletions

File tree

boot-qemu.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
#!/usr/bin/env python3
2+
# pylint: disable=invalid-name
3+
4+
from argparse import ArgumentParser
5+
import os
6+
from pathlib import Path
7+
import platform
8+
import re
9+
import shlex
10+
import shutil
11+
import subprocess
12+
13+
import utils
14+
15+
BOOT_UTILS = Path(__file__).resolve().parent
16+
SUPPORTED_ARCHES = ['x86', 'x86_64']
17+
18+
19+
class QEMURunner:
20+
21+
def __init__(self):
22+
23+
# Properties that can be adjusted by the user or class
24+
self.cmdline = []
25+
self.kernel = None
26+
self.kernel_dir = None
27+
# It may be tempting to use self.use_kvm during initialization of
28+
# subclasses to set certain properties but the user can explicitly opt
29+
# out of KVM after instantiation, so any decisions based on it should
30+
# be confined to run().
31+
self.use_kvm = self._can_use_kvm()
32+
self.smp = 0
33+
34+
self._default_kernel_path = None
35+
self._initrd_arch = None
36+
self._kvm_cpu = 'host'
37+
self._qemu_arch = None
38+
self._qemu_args = [
39+
'-display', 'none',
40+
'-nodefaults',
41+
'-no-reboot',
42+
'-serial', 'mon:stdio',
43+
] # yapf: disable
44+
self._qemu_path = None
45+
self._ram = '512m'
46+
47+
def _can_use_kvm(self):
48+
return False
49+
50+
def _get_default_smp_value(self):
51+
if not self.kernel_dir:
52+
raise RuntimeError('No kernel build folder specified?')
53+
54+
# If kernel_dir is the kernel source, the configuration will be at
55+
# <kernel_dir>/.config
56+
#
57+
# If kernel_dir is the direct parent to the full kernel image, the
58+
# configuration could either be:
59+
# * <kernel_dir>/.config (if the image is vmlinux)
60+
# * <kernel_dir>/../../../.config (if the image is in arch/*/boot/)
61+
# * <kernel_dir>/config (if the image is in a TuxMake folder)
62+
possible_locations = ['.config', '../../../.config', 'config']
63+
configuration = utils.find_first_file(self.kernel_dir,
64+
possible_locations,
65+
required=False)
66+
67+
config_nr_cpus = 8 # sensible default based on treewide defaults,
68+
if configuration:
69+
conf_txt = configuration.read_text(encoding='utf-8')
70+
if (match := re.search(r'CONFIG_NR_CPUS=(\d+)', conf_txt)):
71+
config_nr_cpus = int(match.groups()[0])
72+
73+
# Use the minimum of the number of usable processers for the script or
74+
# CONFIG_NR_CPUS.
75+
usable_cpus = os.cpu_count()
76+
return min(usable_cpus, config_nr_cpus)
77+
78+
def _get_qemu_ver_string(self):
79+
if not self._qemu_path:
80+
raise RuntimeError('No path to QEMU set?')
81+
qemu_ver = subprocess.run([self._qemu_path, '--version'],
82+
capture_output=True,
83+
check=True,
84+
text=True)
85+
return qemu_ver.stdout.splitlines()[0]
86+
87+
def _have_dev_kvm_access(self):
88+
return os.access('/dev/kvm', os.R_OK | os.W_OK)
89+
90+
def _prepare_initrd(self):
91+
if not self._initrd_arch:
92+
raise RuntimeError('No initrd architecture specified?')
93+
if not (src := Path(BOOT_UTILS, 'images', self._initrd_arch,
94+
'rootfs.cpio.zst')):
95+
raise FileNotFoundError(f"initrd ('{src}') does not exist?")
96+
97+
(dst := src.with_suffix('')).unlink(missing_ok=True)
98+
99+
utils.check_cmd('zstd')
100+
subprocess.run(['zstd', '-d', src, '-o', dst, '-q'], check=True)
101+
102+
return dst
103+
104+
def run(self):
105+
# Make sure QEMU binary is configured and available
106+
if not self._qemu_arch:
107+
raise RuntimeError('No QEMU architecture set?')
108+
qemu_bin = f"qemu-system-{self._qemu_arch}"
109+
if not (qemu_path := shutil.which(qemu_bin)):
110+
raise RuntimeError(
111+
f'{qemu_bin} could not be found on your system?')
112+
self._qemu_path = Path(qemu_path)
113+
114+
# Locate kernel if it was not specified
115+
if self.kernel:
116+
if not self.kernel_dir:
117+
self.kernel_dir = self.kernel.parent
118+
else:
119+
if not self.kernel_dir:
120+
raise RuntimeError(
121+
'No kernel image or kernel build folder specified?')
122+
if not self._default_kernel_path:
123+
raise RuntimeError('No default kernel path specified?')
124+
125+
possible_kernel_locations = {
126+
Path(self.kernel_dir,
127+
self._default_kernel_path), # default (kbuild)
128+
Path(self.kernel_dir,
129+
self._default_kernel_path.name), # tuxmake
130+
}
131+
for loc in possible_kernel_locations:
132+
if loc.exists():
133+
self.kernel = loc
134+
break
135+
if not self.kernel:
136+
possible_locations = "', '".join(
137+
str(path) for path in possible_kernel_locations)
138+
raise FileNotFoundError(
139+
f"{self._default_kernel_path.name} could not be found at possible locations ('{possible_locations}')",
140+
)
141+
142+
# Kernel options
143+
if self.cmdline:
144+
self._qemu_args += ['-append', ' '.join(self.cmdline)]
145+
self._qemu_args += ['-kernel', self.kernel]
146+
self._qemu_args += ['-initrd', self._prepare_initrd()]
147+
148+
# KVM
149+
if self.use_kvm:
150+
if not self.smp:
151+
self.smp = self._get_default_smp_value()
152+
self._qemu_args += ['-cpu', self._kvm_cpu, '-enable-kvm']
153+
154+
# Machine specs
155+
self._qemu_args += ['-m', self._ram]
156+
if self.smp:
157+
self._qemu_args += ['-smp', str(self.smp)]
158+
159+
# Show information about QEMU
160+
utils.green(f"QEMU location: \033[0m{self._qemu_path.parent}")
161+
utils.green(f"QEMU version: \033[0m{self._get_qemu_ver_string()}")
162+
163+
# Pretty print and run QEMU command
164+
qemu_cmd = [qemu_path, *self._qemu_args]
165+
print(f"$ {' '.join(shlex.quote(str(elem)) for elem in qemu_cmd)}")
166+
subprocess.run(qemu_cmd, check=True)
167+
168+
169+
class X86QEMURunner(QEMURunner):
170+
171+
def __init__(self):
172+
super().__init__()
173+
174+
self.cmdline += ['console=ttyS0', 'earlycon=uart8250,io,0x3f8']
175+
176+
self._default_kernel_path = Path('arch/x86/boot/bzImage')
177+
self._initrd_arch = 'x86'
178+
self._qemu_arch = 'i386'
179+
180+
def _can_use_kvm(self):
181+
return platform.machine() == 'x86_64' and self._have_dev_kvm_access()
182+
183+
def run(self):
184+
if self.use_kvm:
185+
self._qemu_args += ['-d', 'unimp,guest_errors']
186+
187+
super().run()
188+
189+
190+
class X8664QEMURunner(X86QEMURunner):
191+
192+
def __init__(self):
193+
super().__init__()
194+
195+
self._initrd_arch = self._qemu_arch = 'x86_64'
196+
197+
def run(self):
198+
if not self.use_kvm:
199+
self._qemu_args += ['-cpu', 'Nehalem']
200+
201+
super().run()
202+
203+
204+
def parse_arguments():
205+
parser = ArgumentParser(description='Boot a Linux kernel in QEMU')
206+
207+
parser.add_argument(
208+
'-a',
209+
'--architecture',
210+
choices=SUPPORTED_ARCHES,
211+
help='The architecture to boot. Possible values are: %(choices)s',
212+
metavar='ARCH',
213+
required=True)
214+
parser.add_argument(
215+
'-k',
216+
'--kernel-location',
217+
required=True,
218+
help='Absolute or relative path to kernel image or build folder.')
219+
parser.add_argument(
220+
'--no-kvm',
221+
action='store_true',
222+
help='Do not use KVM for accelration even when supported.')
223+
224+
return parser.parse_args()
225+
226+
227+
if __name__ == '__main__':
228+
args = parse_arguments()
229+
230+
arch_to_runner = {
231+
'x86': X86QEMURunner,
232+
'x86_64': X8664QEMURunner,
233+
}
234+
runner = arch_to_runner[args.architecture]()
235+
236+
if not (kernel_location := Path(args.kernel_location).resolve()).exists():
237+
raise FileNotFoundError(
238+
f"Supplied kernel location ('{kernel_location}') does not exist!")
239+
if kernel_location.is_file():
240+
runner.kernel = kernel_location
241+
else:
242+
runner.kernel_dir = kernel_location
243+
244+
if args.no_kvm:
245+
runner.use_kvm = False
246+
247+
runner.run()

0 commit comments

Comments
 (0)