blob: f95ceb63cc20f1bb17b5211662cf5ef8c103bf5b [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2020 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import difflib
import json
import os
import platform
import shlex
import subprocess
import sys
# Special values identifying a PE (i.e., Portable Executable) image, of which
# UEFI executables are examples.
#
# In identifying a UEFI executable, it is insufficient to match based on magic
# alone; ARM Linux boot firmware (e.g., our arm64 boot shim) uses that same
# value.
#
# See https://learn.microsoft.com/en-us/windows/win32/debug/pe-format.
PE_MAGIC = b"MZ"
PE_SIGNATURE = b"PE\0\0"
def is_pe(filepath):
if not os.path.exists(filepath):
return False
with open(filepath, "rb") as f:
if f.read(2) != PE_MAGIC:
return False
f.seek(0x3C)
signature_offset = int.from_bytes(f.read(4), byteorder="little")
f.seek(signature_offset)
return f.read(4) == PE_SIGNATURE
class BootTest(object):
def __init__(self, images_by_label, test_json, build_dir):
self.build_dir = build_dir
test = test_json["test"]
self.name = test["name"]
self.label = test["label"]
self.path = test["path"]
images = test_json["image_overrides"]
self.zbi = images_by_label[images["zbi"]] if "zbi" in images else None
self.qemu_kernel = (
images_by_label[images["qemu_kernel"]]
if "qemu_kernel" in images else None)
self.efi_disk = (
images_by_label[images["efi_disk"]]
if "efi_disk" in images else None)
arch_image = self.qemu_kernel or self.efi_disk or self.zbi or None
self.arch = arch_image.get("cpu", None) if arch_image else None
# Enables sorting by name.
def __lt__(self, other):
return self.name < other.name
@staticmethod
def is_boot_test(test_json):
return "image_overrides" in test_json
def is_uefi_boot(self):
if self.efi_disk:
return True
# The QEMU kernel might be a UEFI executable. Look for PE magic.
if not self.qemu_kernel:
return False
kernel = os.path.join(self.build_dir, self.qemu_kernel["path"])
return is_pe(kernel)
def print(self, command=None):
kinds = []
if self.is_uefi_boot():
kinds.append("UEFI")
if self.qemu_kernel:
kinds.append("QEMU kernel")
if self.zbi:
kinds.append("ZBI")
if self.efi_disk:
kinds.append("EFI disk")
print("* %s (%s)" % (self.name, ", ".join(kinds)))
print(" label: %s" % self.label)
print(" cpu: %s" % (self.arch or "Unknown!"))
if self.qemu_kernel:
print(" qemu kernel: %s" % self.qemu_kernel["path"])
if self.zbi:
print(" zbi: %s" % self.zbi["path"])
if self.efi_disk:
print(" efi disk: %s" % self.efi_disk["path"])
if command:
print(" command: %s" % " ".join(map(shlex.quote, command)))
def error(str):
RED = "\033[91m"
END = "\033[0m"
print(RED + "ERROR: " + str + END)
def warning(str):
YELLOW = "\033[93m"
END = "\033[0m"
print(YELLOW + "WARNING: " + str + END)
def find_bootserver(build_dir):
host_os = {"Linux": "linux", "Darwin": "mac"}[platform.system()]
host_cpu = {"x86_64": "x64", "arm64": "arm64"}[platform.machine()]
with open(os.path.join(build_dir, "tool_paths.json")) as file:
tool_paths = json.load(file)
bootservers = [
os.path.join(build_dir, tool["path"]) for tool in tool_paths if (
tool["name"] == "bootserver" and tool["cpu"] == host_cpu and
tool["os"] == host_os)
]
if bootservers:
return bootservers[0]
print("Cannot find bootserver for %s-%s" % (host_os, host_cpu))
sys.exit(1)
EPILOG = """
In order to use this tool, please ensure that your boot test (usually defined
by one of zbi_test(), qemu_kernel_test(), or efi_test()) is in your GN graph. A
way to do this is to add //bundles:boot_tests to your `fx set` invocation.
"""
def main():
parser = argparse.ArgumentParser(
prog="fx run-boot-test", description="Run a boot test.", epilog=EPILOG)
modes = parser.add_mutually_exclusive_group()
modes.add_argument(
"--boot", "-b", action="store_true", help="Run via bootserver")
parser.add_argument(
"--args",
"-a",
metavar="RUNNER-ARG",
action="append",
default=[],
help="Pass RUNNER-ARG to bootserver/fx qemu",
)
parser.add_argument(
"--cmdline",
"-c",
metavar="KERNEL-ARGS",
action="append",
default=[],
help="Add kernel command-line arguments.",
)
parser.add_argument(
"name",
help="Name of the boot test (target) to run",
nargs="?",
)
parser.add_argument(
"--arch",
help="CPU architecture to run",
metavar="ARCH",
default=os.getenv("FUCHSIA_ARCH"),
)
args = parser.parse_args()
build_dir = os.path.relpath(os.getenv("FUCHSIA_BUILD_DIR"))
if build_dir is None:
print("FUCHSIA_BUILD_DIR not set")
return 1
if args.arch is None:
print("FUCHSIA_ARCH not set")
return 1
# Construct a map of images by GN label. Boot test metadata records its
# desired images that way.
with open(os.path.join(build_dir, "images.json")) as file:
images = {}
for image in json.load(file):
images[image["label"]] = image
# There can be multiple versions of the same boot test for different host
# architectures. These will otherwise only differ in metadata name, a
# difference that `BootTest()` normalizes away.
with open(os.path.join(build_dir, "tests.json")) as file:
boot_tests = {}
for test in json.load(file):
if BootTest.is_boot_test(test):
boot_test = BootTest(images, test, build_dir)
if boot_test.arch == args.arch:
boot_tests[boot_test.name] = boot_test
if not boot_tests:
warning(
"no boot tests found. Is //bundles:boot_tests in your GN graph?")
return 0
if not args.name:
for test in sorted(boot_tests.values()):
test.print()
return 0
names = [test.name for test in boot_tests.values()]
# A cut-off of 0.8 was determined to be good enough in experimenting
# with input names against "core-tests".
matching_names = difflib.get_close_matches(args.name, names, cutoff=0.8)
matches = [boot_tests[name] for name in matching_names]
if len(matches) == 0:
error("no boot tests closely matching a name of '%s' found" % args.name)
return 1
# The returned matches will be ordered by similarlity; if we have an exact
# match, always go with that.
elif len(matches) > 1 and matches[0].name != args.name:
error(
"no boot tests closely matching a name of '%s' found. Closest matches:"
% args.name)
for test in matches:
test.print()
return 1
test = matches[0]
if args.boot:
if test.qemu_kernel:
print(error("cannot use --boot with QEMU-only test %s" % test.name))
return 1
assert test.zbi
bootserver = find_bootserver(build_dir)
cmd = [bootserver, "--boot"] + test.zbi["path"] + args.args
else:
cmd = ["fx", "qemu", "--arch", args.arch] + args.args
if test.is_uefi_boot():
cmd += ["--uefi"]
if test.qemu_kernel:
cmd += ["-t", test.qemu_kernel["path"]]
if test.zbi:
cmd += ["-z", test.zbi["path"]]
if test.efi_disk:
cmd += ["-D", test.efi_disk["path"]]
for arg in args.cmdline:
cmd += ["-c", arg]
if not args.boot:
# Prevents QEMU from boot-looping, as most boot tests do not have a
# means of gracefully shutting down.
cmd += ["--", "-no-reboot"]
test.print(command=cmd)
return subprocess.run(cmd, cwd=build_dir).returncode
if __name__ == "__main__":
sys.exit(main())