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>
279 lines
11 KiB
Python
Executable File
279 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0+
|
|
"""
|
|
Script to build an EFI thing suitable for booting with QEMU, possibly running
|
|
it also.
|
|
|
|
UEFI binaries for QEMU used for testing this script:
|
|
|
|
OVMF-pure-efi.i386.fd at
|
|
https://drive.google.com/file/d/1jWzOAZfQqMmS2_dAK2G518GhIgj9r2RY/view?usp=sharing
|
|
|
|
OVMF-pure-efi.x64.fd at
|
|
https://drive.google.com/file/d/1c39YI9QtpByGQ4V0UNNQtGqttEzS-eFV/view?usp=sharing
|
|
|
|
Use ~/.build-efi to configure the various paths used by this script.
|
|
|
|
When --bootcmd is specified, a uboot.env file is created on the EFI partition
|
|
containing the boot command. U-Boot needs to be configured to import this file
|
|
on startup, for example by adding to CONFIG_PREBOOT or the default bootcmd:
|
|
|
|
load ${devtype} ${devnum}:${distro_bootpart} ${loadaddr} uboot.env; \
|
|
env import -t ${loadaddr} ${filesize}
|
|
"""
|
|
|
|
from argparse import ArgumentParser
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
|
|
import build_helper
|
|
|
|
# pylint: disable=C0413
|
|
from u_boot_pylib import command
|
|
from u_boot_pylib import tools
|
|
from u_boot_pylib import tout
|
|
|
|
|
|
def parse_args():
|
|
"""Parse the program arguments
|
|
|
|
Return:
|
|
Namespace object
|
|
"""
|
|
parser = ArgumentParser(
|
|
epilog='Script for running U-Boot as an EFI app/payload')
|
|
build_helper.add_common_args(parser)
|
|
parser.add_argument('-g', '--debug', action='store_true',
|
|
help="Run QEMU with gdb")
|
|
parser.add_argument('--write-kernel', action='store_true',
|
|
help='Add a kernel to the disk image')
|
|
parser.add_argument('-O', '--old', action='store_true',
|
|
help='Use old EFI app build (before 32/64 split)')
|
|
parser.add_argument('-p', '--payload', action='store_true',
|
|
help='Package up the payload instead of the app')
|
|
parser.add_argument('-P', '--partition', action='store_true',
|
|
help='Create a partition table')
|
|
parser.add_argument('--spice', action='store_true',
|
|
help='Enable SPICE for clipboard sharing')
|
|
parser.add_argument('-N', '--net', action='store_true',
|
|
help='Enable networking (with SSH forwarding on port 2222)')
|
|
parser.add_argument('-v', '--verbose', action='store_true',
|
|
help='Show executed commands')
|
|
parser.add_argument('--include-dir',
|
|
help='Directory containing additional files to include in the image')
|
|
|
|
args = parser.parse_args()
|
|
|
|
return args
|
|
|
|
|
|
class BuildEfi:
|
|
"""Class to collect together the various bits of state while running"""
|
|
def __init__(self, args):
|
|
self.helper = build_helper.Helper(args)
|
|
self.helper.read_settings()
|
|
self.img_fname = self.helper.get_setting('efi_image_file', 'efi.img')
|
|
self.img = None
|
|
self.build_topdir = self.helper.get_setting("build_dir", '/tmp')
|
|
self.build_dir = None
|
|
self.args = args
|
|
self.imagedir = Path(self.helper.get_setting('image_dir', '~/dev'))
|
|
|
|
def run_qemu(self, serial_only):
|
|
"""Run QEMU
|
|
|
|
Args:
|
|
serial_only (bool): True to run without a display
|
|
"""
|
|
extra = []
|
|
efi_dir = self.helper.get_setting('efi_dir')
|
|
if self.args.arch == 'arm':
|
|
qemu_arch = 'aarch64'
|
|
extra += ['--machine', 'virt', '-cpu', 'max']
|
|
bios = os.path.join(efi_dir, 'OVMF-pure-efi.aarch64.fd.64m')
|
|
var_store = os.path.join(efi_dir, 'varstore.img')
|
|
extra += [
|
|
'-drive', f'if=pflash,format=raw,file={bios},readonly=on',
|
|
'-drive', f'if=pflash,format=raw,file={var_store}'
|
|
]
|
|
extra += ['-drive',
|
|
f'if=virtio,file={self.img},format=raw,id=hd0']
|
|
else: # x86
|
|
if self.helper.bitness == 64:
|
|
qemu_arch = 'x86_64'
|
|
bios = 'OVMF-release-x64.fd'
|
|
else:
|
|
qemu_arch = 'i386'
|
|
bios = 'OVMF-pure-efi.i386.fd'
|
|
bios = os.path.join(efi_dir, bios)
|
|
var_store = os.path.join(efi_dir, 'OVMF_VARS_4M.fd')
|
|
extra += [
|
|
'-drive', f'if=pflash,format=raw,file={bios},readonly=on',
|
|
'-drive', f'if=pflash,format=raw,file={var_store}'
|
|
]
|
|
extra += ['-drive', f'id=disk,file={self.img},if=none,format=raw']
|
|
extra += ['-device', 'ahci,id=ahci']
|
|
extra += ['-device', 'ide-hd,drive=disk,bus=ahci.0']
|
|
qemu = f'qemu-system-{qemu_arch}'
|
|
if serial_only:
|
|
extra += ['-display', 'none', '-serial', 'mon:stdio']
|
|
serial_msg = ' (Ctrl-a x to quit)'
|
|
else:
|
|
if self.args.arch == 'arm':
|
|
extra += ['-device', 'virtio-gpu-pci']
|
|
extra += ['-device', 'qemu-xhci', '-device', 'usb-kbd',
|
|
'-device', 'usb-tablet']
|
|
extra += ['-display', 'default,show-cursor=on']
|
|
else: # x86
|
|
extra += ['-device', 'qemu-xhci', '-device', 'usb-kbd',
|
|
'-device', 'usb-mouse']
|
|
|
|
# This uses QEMU's GTK clipboard integration with SPICE vdagent
|
|
if self.args.spice:
|
|
extra += ['-device', 'virtio-serial-pci']
|
|
extra += ['-chardev', 'qemu-vdagent,id=spicechannel0,name=vdagent,clipboard=on']
|
|
extra += ['-device', 'virtserialport,chardev=spicechannel0,name=com.redhat.spice.0']
|
|
extra += ['-serial', 'mon:stdio']
|
|
serial_msg = ''
|
|
if self.args.kvm:
|
|
extra.extend(['-enable-kvm', '-cpu', 'host'])
|
|
|
|
print(f'Running {qemu}{serial_msg}')
|
|
|
|
# Use 512MB since U-Boot EFI likes to have 256MB to play with
|
|
if self.args.os or self.args.disk:
|
|
mem = '4G'
|
|
extra.extend(['-smp', '4'])
|
|
else:
|
|
mem = '1G'
|
|
|
|
if self.args.debug:
|
|
extra.extend(['-s', '-S'])
|
|
|
|
cmd = [qemu]
|
|
cmd += '-m', mem
|
|
if self.args.net:
|
|
cmd += '-netdev', 'user,id=net0,hostfwd=tcp::2222-:22'
|
|
cmd += '-device', 'virtio-net-pci,netdev=net0'
|
|
else:
|
|
cmd += '-nic', 'none'
|
|
cmd += extra
|
|
self.helper.add_qemu_args(self.args, cmd, base_hd=1)
|
|
tout.info(' '.join(cmd))
|
|
sys.stdout.flush()
|
|
command.run(*cmd)
|
|
|
|
def setup_files(self, build, build_type, dst):
|
|
"""Set up files in the staging area
|
|
|
|
Args:
|
|
build (str): Name of build being packaged, e.g. 'efi-x86_app32'
|
|
build_type (str): Build type ('app' or 'payload')
|
|
dst (str): Destination directory
|
|
"""
|
|
print(f'Packaging {build}')
|
|
if self.args.custom:
|
|
dirname, fname = os.path.split(self.args.custom)
|
|
if not dirname:
|
|
dirname = '.'
|
|
else:
|
|
fname = f'u-boot-{build_type}.efi'
|
|
dirname = f'{self.build_dir}/'
|
|
tools.write_file(f'{dst}/startup.nsh', f'fs0:{fname}', binary=False)
|
|
shutil.copy(f'{dirname}/{fname}', dst)
|
|
|
|
# Copy additional files from include directory if specified
|
|
if self.args.include_dir:
|
|
include_path = Path(self.args.include_dir)
|
|
if include_path.exists() and include_path.is_dir():
|
|
print(f'Including files from {include_path}')
|
|
for item in include_path.iterdir():
|
|
if item.is_file():
|
|
print(f' Copying {item.name}')
|
|
shutil.copy(str(item), dst)
|
|
elif item.is_dir():
|
|
dest_dir = Path(dst) / item.name
|
|
print(f' Copying directory {item.name}')
|
|
shutil.copytree(str(item), str(dest_dir))
|
|
else:
|
|
print(f'Warning: Include directory {include_path} does not exist or is not a directory')
|
|
|
|
# Write U-Boot environment file if bootcmd is specified
|
|
if self.args.bootcmd:
|
|
# Check if mkenvimage is available (local build or system-wide)
|
|
mkenvimage = 'tools/mkenvimage'
|
|
if not os.path.exists(mkenvimage):
|
|
mkenvimage = 'mkenvimage'
|
|
if not shutil.which(mkenvimage):
|
|
tout.error('Please install u-boot-tools package:')
|
|
tout.error(' sudo apt install u-boot-tools')
|
|
raise FileNotFoundError('mkenvimage not found')
|
|
|
|
# Create text environment file
|
|
env_content = f'bootcmd={self.args.bootcmd}\n'
|
|
with tempfile.NamedTemporaryFile(mode='w', delete=False,
|
|
suffix='.txt') as outf:
|
|
outf.write(env_content)
|
|
env_fname = outf.name
|
|
|
|
try:
|
|
# Convert to binary format with CRC using mkenvimage
|
|
command.run(mkenvimage, '-s', '0x1000',
|
|
'-o', f'{dst}/uboot.env', env_fname)
|
|
print(f'Created uboot.env with bootcmd: {self.args.bootcmd}')
|
|
finally:
|
|
os.unlink(env_fname)
|
|
|
|
def do_build(self, build):
|
|
"""Build U-Boot for the selected board"""
|
|
extra = ['-a', '~CONSOLE_PAGER'] if self.args.no_pager else []
|
|
res = command.run_one('buildman', '-w', '-o', self.build_dir, *extra,
|
|
'--board', build, '-I', raise_on_error=False)
|
|
if res.return_code and res.return_code != 101: # Allow warnings
|
|
raise ValueError(
|
|
f'buildman exited with {res.return_code}: {res.combined}')
|
|
|
|
def start(self):
|
|
"""This does all the work"""
|
|
args = self.args
|
|
arch = 'arm' if self.args.arch == 'arm' else 'x86'
|
|
build_type = 'payload' if args.payload else 'app'
|
|
build = f'efi-{arch}_{build_type}{self.helper.bitness}'
|
|
|
|
if args.build_dir:
|
|
self.build_dir = args.build_dir
|
|
self.img = f'{self.build_dir}/{self.img_fname}'
|
|
else:
|
|
self.build_dir = f'{self.build_topdir}/{build}'
|
|
self.img = self.img_fname
|
|
if not args.no_build:
|
|
self.do_build(build)
|
|
|
|
if args.old and self.helper.bitness == 32:
|
|
build = f'efi-{arch}_{build_type}'
|
|
|
|
with self.helper.make_disk(self.img, fs_type='vfat',
|
|
use_part=args.partition) as dirpath:
|
|
self.setup_files(build, build_type, dirpath)
|
|
if self.args.write_kernel:
|
|
bzimage = self.helper.get_setting('bzimage_file', 'bzImage')
|
|
command.run('cp', bzimage, f'{dirpath}/vmlinuz')
|
|
|
|
if args.run:
|
|
self.run_qemu(args.serial_only)
|
|
|
|
|
|
def main():
|
|
"""Parse arguments and start the program"""
|
|
args = parse_args()
|
|
tout.init(tout.INFO if args.verbose else tout.WARNING)
|
|
|
|
qemu = BuildEfi(args)
|
|
qemu.start()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|