Files
u-boot/scripts/build-qemu
Simon Glass 3a801fde21 scripts: Provide a way to use custom firmware
Update both build-efi and build-scripts to allow a -c option to provide
custom firmware. This makes the scripts more generally useful.

Drop the existing -c for --spice since it conflicts and is also is bit
hard to remember.

Signed-off-by: Simon Glass <simon.glass@canonical.com>
2025-11-12 06:09:52 -07:00

330 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0+
#
"""Script to build/run U-Boot with QEMU
It assumes that
- you build U-Boot in ${ubdir}/<name> where <name> is the U-Boot
board config
- your OS images are in ${imagedir}/{distroname}/
So far the script supports only ARM and x86
"""
import argparse
import os
from pathlib import Path
import subprocess
import sys
import shlex
import time
import build_helper
OUR_PATH = os.path.dirname(os.path.realpath(__file__))
OUR1_PATH = os.path.dirname(OUR_PATH)
# Bring in the patman and dtoc libraries (but don't override the first path
# in PYTHONPATH)
sys.path.insert(2, os.path.join(OUR1_PATH, 'tools'))
# pylint: disable=C0413
from u_boot_pylib import command
from u_boot_pylib import tools
from u_boot_pylib import tout
def parse_args():
"""Parses command-line arguments"""
parser = argparse.ArgumentParser(
description='Build and/or run U-Boot with QEMU',
formatter_class=argparse.RawTextHelpFormatter)
build_helper.add_common_args(parser)
parser.add_argument('-e', '--sct-run', action='store_true',
help='Run UEFI Self-Certification Test (SCT)')
parser.add_argument('-E', '--use-tianocore', action='store_true',
help='Run Tianocore (OVMF) instead of U-Boot')
parser.add_argument('-Q', '--use-qboot', action='store_true',
help='Run qboot instead of U-Boot')
parser.add_argument('-x', '--xpl', action='store_true',
help='Use xPL image rather than U-Boot proper')
parser.add_argument('-T', '--tkey', action='store_true',
help='Enable TKey USB passthrough for testing')
parser.add_argument(
'--sct-seq',
help='SCT sequence-file to be written into the SCT image if -e')
parser.add_argument('-v', '--verbose', action='store_true',
help='Show executed commands')
return parser.parse_args()
class BuildQemu:
"""Build and/or run U-Boot with QEMU based on command line arguments"""
def __init__(self, args):
"""Set up arguments and configure paths"""
self.args = args
self.helper = build_helper.Helper(args)
self.helper.read_settings()
self.ubdir = Path(self.helper.get_setting('build_dir', '/tmp/b'))
self.sctdir = Path(self.helper.get_setting('sct_dir', '~/dev/efi/sct'))
self.tiano = Path(self.helper.get_setting('tianocore_dir',
'~/dev/tiano'))
self.qboot = Path(self.helper.get_setting('qboot_dir', '~/dev/qboot'))
self.mnt = Path(self.helper.get_setting('sct_mnt', '/mnt/sct'))
self.qemu_extra = []
self.helper.mem = '512M' # Default QEMU memory
if args.disk:
self.helper.mem = '4G'
self.qemu_extra.extend(['-smp', '4'])
if args.sct_run:
self.helper.mem = '4G'
self.qemu_extra.extend(['-smp', '4'])
# For potential interaction within SCT
self.qemu_extra.extend(['-device', 'qemu-xhci'])
self.qemu_extra.extend(['-device', 'usb-kbd'])
sct_image_path = self.sctdir / 'sct.img'
if not sct_image_path.exists():
tout.fatal(f'Error: SCT image {sct_image_path} not found, '
'required for -e')
self.qemu_extra.extend([
'-drive', f'file={sct_image_path},format=raw,if=none,id=vda',
'-device', 'virtio-blk-pci,drive=vda,bootindex=1'])
# Basic networking for SCT, if needed
self.qemu_extra.extend([
'-device', 'virtio-net-pci,netdev=net0',
'-netdev', 'user,id=net0'])
args.serial_only = True # SCT implies serial output
if args.os:
self.helper.mem = '4G'
self.qemu_extra.extend(['-smp', '4'])
self.kvm_params = []
if args.kvm:
self.kvm_params = ['-enable-kvm', '-cpu', 'host']
bios_override = None
if args.custom:
bios_override = Path(args.custom)
if not bios_override.exists():
tout.fatal(
'Error: Custom BIOS specified (-c) but not found at '
f'{bios_override}')
elif args.use_tianocore:
if args.arch == 'arm':
bios_override = Path(self.tiano, 'OVMF-pure-efi.aarch64.fd.64m')
else:
bios_override = Path(self.tiano, 'OVMF-pure-efi.x64.fd')
if not bios_override.exists():
tout.fatal(
'Error: Tianocore BIOS specified (-E) but not found at '
f'{bios_override}')
elif args.use_qboot:
bios_override = Path(self.qboot, 'bios.bin')
if not bios_override.exists():
tout.fatal(
'Error: qboot BIOS specified (-Q) but not found at '
f'{bios_override}')
self.seq_fname = Path(args.sct_seq) if args.sct_seq else None
# arch-specific setup
if args.arch == 'arm':
if args.xpl:
self.board = 'qemu_arm_spl'
default_bios = 'image.bin'
else:
self.board = 'qemu_arm'
default_bios = 'u-boot.bin'
self.helper.qemu = 'qemu-system-arm'
self.qemu_extra.extend(['-machine', 'virt'])
if not args.kvm:
self.qemu_extra.extend(['-accel', 'tcg'])
if self.helper.bitness == 64:
if args.xpl:
self.board = 'qemu_arm64_spl'
else:
self.board = 'qemu_arm64'
self.helper.qemu = 'qemu-system-aarch64'
self.qemu_extra.extend(['-cpu', 'cortex-a57'])
elif args.arch == 'x86':
self.board = 'qemu-x86'
default_bios = 'u-boot.rom'
self.helper.qemu = 'qemu-system-i386'
self.qemu_extra.extend(['-machine', 'q35'])
if args.tkey:
# Pass through TKey USB device to QEMU
self.qemu_extra.extend(['-device', 'usb-host,vendorid=0x1207,productid=0x8887'])
if self.helper.bitness == 64:
self.board = 'qemu-x86_64'
self.helper.qemu = 'qemu-system-x86_64'
else:
raise ValueError(f"Invalid arch '{args.arch}'")
self.build_dir = self.ubdir / self.board
self.bios = (bios_override if bios_override
else self.build_dir / default_bios)
@staticmethod
def execute_command(cmd_list, desc, check=True, **kwargs):
"""Execute a shell command and handle errors
Args:
cmd_list (list of str): The command and its arguments as a list
desc (str): A description of the command being executed
check (bool): Raise CalledProcessError on non-zero exit code
kwargs: Additional arguments for subprocess.run
Return:
subprocess.CompletedProcess: The result of the subprocess.run call
Raises:
SystemExit: If the command is not found or fails and check is True
"""
tout.info(f"Executing: {desc} -> {shlex.join(cmd_list)}")
try:
# Decode stdout/stderr by default if text=True
if 'text' not in kwargs:
kwargs['text'] = True
return subprocess.run(cmd_list, check=check, **kwargs)
except FileNotFoundError:
tout.fatal(f"Error: Command '{cmd_list[0]}' not found")
except subprocess.CalledProcessError as proc:
tout.error(f'Error {desc}: Command failed with exit code '
f'{proc.returncode}')
if proc.stdout:
tout.error(f'Stdout:\n{proc.stdout}')
if proc.stderr:
tout.error(f'Stderr:\n{proc.stderr}')
tout.fatal('Failed')
def build_u_boot(self):
"""Build U-Boot using buildman
"""
self.build_dir.mkdir(parents=True, exist_ok=True)
cmd = ['buildman', '-w', '-o', str(self.build_dir), '--board',
self.board, '-I']
if self.args.no_pager:
cmd += ['-a', '~CONSOLE_PAGER']
self.execute_command(
cmd,
f'Building U-Boot for {self.board} in {self.build_dir}')
def update_sct_sequence(self):
"""Update the SCT image with a specified sequence file
Requires sudo for loop device setup and mounting
"""
if not (self.args.sct_run and self.seq_fname and
self.seq_fname.exists()):
if (self.args.sct_run and self.seq_fname and
not self.seq_fname.exists()):
tout.warning(f'Warning: SCT sequence file {self.seq_fname}'
'not found')
return
fname = self.sctdir / 'sct.img'
if not fname.exists():
tout.fatal(f'Error: SCT image {fname} not found')
loopdev = None
try:
# Find free loop device and attach
loopdev = command.output_one_line(
'sudo', 'losetup', '--show', '-f', '-P', str(fname))
partition_path_str = f'{loopdev}p1'
uid, gid = os.getuid(), os.getgid()
mount_cmd = ['sudo', 'mount', partition_path_str,
str(self.mnt), '-o', f'uid={uid},gid={gid},rw']
mount_cmd.extend(['-t', 'vfat'])
self.execute_command(mount_cmd,
f'Mounting {partition_path_str} to {self.mnt}')
target_sct_path = self.mnt / self.seq_fname.name
self.execute_command(
['sudo', 'cp', str(self.seq_fname), str(target_sct_path)],
f'Copying {self.seq_fname.name} to {self.mnt}'
)
tout.info(f"Copied {self.seq_fname} to {target_sct_path}")
finally:
if Path(self.mnt).is_mount():
self.execute_command(['sudo', 'umount', str(self.mnt)],
f'Unmounting {self.mnt}', check=False)
if loopdev:
self.execute_command(['sudo', 'losetup', '-d', loopdev],
f'Detaching loop device {loopdev}',
check=False)
def run_qemu(self):
"""Construct and run the QEMU command"""
if not self.bios.exists():
tout.fatal(f"Error: BIOS file '{self.bios}' not found")
qemu_cmd = [str(self.helper.qemu)]
if self.bios:
qemu_cmd.extend(['-bios', str(self.bios)])
qemu_cmd.extend(self.kvm_params)
qemu_cmd.extend(['-m', self.helper.mem])
if not self.args.sct_run and not self.args.use_qboot:
qemu_cmd.extend(['-netdev', 'user,id=net0,hostfwd=tcp::2222-:22',
'-device', 'virtio-net-pci,netdev=net0'])
# SCT usually runs headlessly
if self.args.serial_only or self.args.sct_seq:
qemu_cmd.extend(['-display', 'none'])
elif self.args.arch == 'arm':
qemu_cmd.extend(['-device', 'virtio-gpu-pci'])
qemu_cmd.extend(['-device', 'qemu-xhci', '-device', 'usb-kbd',
'-device', 'usb-tablet', '-device', 'usb-mouse'])
qemu_cmd.extend(['-display', 'default,show-cursor=on'])
elif self.args.arch == 'x86':
qemu_cmd.extend(['-device', 'qemu-xhci'])
qemu_cmd.extend(['-device', 'usb-kbd', '-device', 'usb-tablet'])
qemu_cmd.extend(['-display', 'default,show-cursor=on'])
if not any(item.startswith('-serial') for item in self.qemu_extra):
qemu_cmd.extend(['-serial', 'mon:stdio'])
self.helper.add_qemu_args(self.args, qemu_cmd)
# Add other parameters gathered from options
qemu_cmd.extend(self.qemu_extra)
self.helper.setup_share(qemu_cmd)
self.helper.run(qemu_cmd)
def start(self):
"""Build and run QEMU"""
if not self.args.no_build and not self.args.use_tianocore:
self.build_u_boot()
# Update SCT sequence if -e and -S are given
if self.args.sct_run and self.seq_fname:
self.update_sct_sequence()
self.run_qemu()
def main():
"""Parse arguments and start the program"""
args = parse_args()
tout.init(tout.INFO if args.verbose else tout.WARNING)
qemu = BuildQemu(args)
qemu.start()
if __name__ == '__main__':
main()