#!/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}/ where 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()