Merge manifest into the board DT, add boot flow drivers

Hafnium currently supports two kinds of boot flow:
(a) Linux-like, where the board FDT is passed by the boot loader to
the OS kernel in first kernel arg (this is used by QEMU, FVP, RPi, ...),
(b) Android-like, where the FDT used by Hafnium is compiled into it and
the VMs have their own.

This used to be implemented using weak symbols and each board spec
overriding it with its own implementation. This patch introduces the
concept of boot-flow drivers in the main tree, and the corresponding
driver selected using a config option in board spec. Drivers for the two
boot-flows described above are implemented.

Simultaneously, the manifest is now read from the FDT available at
Hafnium init time. This required two notable changes:
(1) all values are copied into the manifest struct because FDT is
unmapped, modified and passed to the primary VM,
(2) manifest is now written as an overlay; QEMU and FVP test drivers
overlay it automatically.

Bug: 117551352
Change-Id: Ieae7fe4ef5b3047174ec0ad057e487660ccd5a03
diff --git a/test/hftest/hftest.py b/test/hftest/hftest.py
index 4192cef..a234a99 100755
--- a/test/hftest/hftest.py
+++ b/test/hftest/hftest.py
@@ -113,6 +113,7 @@
         "artifacts",
         "kernel",
         "initrd",
+        "manifest",
         "vm_args",
     ])
 
@@ -121,9 +122,16 @@
 # a single invocation of the target platform.
 DriverRunState = collections.namedtuple("DriverRunState", [
         "log_path",
+        "ret_code",
     ])
 
 
+class DriverRunException(Exception):
+    """Exception thrown if subprocess invoked by a driver returned non-zero
+    status code. Used to fast-exit from a driver command sequence."""
+    pass
+
+
 class Driver:
     """Parent class of drivers for all testable platforms."""
 
@@ -137,28 +145,34 @@
     def start_run(self, run_name):
         """Hook called by Driver subclasses before they invoke the target
         platform."""
-        return DriverRunState(self.args.artifacts.create_file(run_name, ".log"))
+        return DriverRunState(
+                self.args.artifacts.create_file(run_name, ".log"), 0)
 
     def exec_logged(self, run_state, exec_args):
         """Run a subprocess on behalf of a Driver subclass and append its
         stdout and stderr to the main log."""
+        assert(run_state.ret_code == 0)
         with open(run_state.log_path, "a") as f:
             f.write("$ {}\r\n".format(" ".join(exec_args)))
             f.flush()
-            return subprocess.call(exec_args, stdout=f, stderr=f)
+            ret_code = subprocess.call(exec_args, stdout=f, stderr=f)
+            if ret_code != 0:
+                run_state = DriverRunState(run_state.log_path, ret_code)
+                raise DriverRunException()
 
-    def finish_run(self, run_state, ret_code):
+    def finish_run(self, run_state):
         """Hook called by Driver subclasses after they finished running the
         target platform. `ret_code` argument is the return code of the main
         command run by the driver. A corresponding log message is printed."""
         # Decode return code and add a message to the log.
         with open(run_state.log_path, "a") as f:
-            if ret_code == 124:
+            if run_state.ret_code == 124:
                 f.write("\r\n{}{} timed out\r\n".format(
                     HFTEST_LOG_PREFIX, HFTEST_LOG_FAILURE_PREFIX))
-            elif ret_code != 0:
+            elif run_state.ret_code != 0:
                 f.write("\r\n{}{} process return code {}\r\n".format(
-                    HFTEST_LOG_PREFIX, HFTEST_LOG_FAILURE_PREFIX, ret_code))
+                    HFTEST_LOG_PREFIX, HFTEST_LOG_FAILURE_PREFIX,
+                    run_state.ret_code))
 
         # Append log of this run to full test log.
         log_content = read_file(run_state.log_path)
@@ -167,6 +181,14 @@
             log_content + "\r\n\r\n")
         return log_content
 
+    def overlay_dtb(self, run_state, base_dtb, overlay_dtb, out_dtb):
+        """Overlay `overlay_dtb` over `base_dtb` into `out_dtb`."""
+        dtc_args = [
+            DTC_SCRIPT, "overlay",
+            out_dtb, base_dtb, overlay_dtb,
+        ]
+        self.exec_logged(run_state, dtc_args)
+
 
 class QemuDriver(Driver):
     """Driver which runs tests in QEMU."""
@@ -174,7 +196,7 @@
     def __init__(self, args):
         Driver.__init__(self, args)
 
-    def gen_exec_args(self, test_args):
+    def gen_exec_args(self, test_args, dtb_path=None, dumpdtb_path=None):
         """Generate command line arguments for QEMU."""
         exec_args = [
             "timeout", "--foreground", "10s",
@@ -186,6 +208,12 @@
             "-kernel", self.args.kernel,
         ]
 
+        if dtb_path:
+            exec_args += ["-dtb", dtb_path]
+
+        if dumpdtb_path:
+            exec_args += ["-machine", "dumpdtb=" + dumpdtb_path]
+
         if self.args.initrd:
             exec_args += ["-initrd", self.args.initrd]
 
@@ -195,11 +223,33 @@
 
         return exec_args
 
+    def dump_dtb(self, run_state, test_args, path):
+        dumpdtb_args = self.gen_exec_args(test_args, dumpdtb_path=path)
+        self.exec_logged(run_state, dumpdtb_args)
+
     def run(self, run_name, test_args):
         """Run test given by `test_args` in QEMU."""
         run_state = self.start_run(run_name)
-        ret_code = self.exec_logged(run_state, self.gen_exec_args(test_args))
-        return self.finish_run(run_state, ret_code)
+
+        try:
+            dtb_path = None
+
+            # If manifest DTBO specified, dump DTB from QEMU and overlay them.
+            if self.args.manifest:
+                base_dtb_path = self.args.artifacts.create_file(
+                    run_name, ".base.dtb")
+                dtb_path = self.args.artifacts.create_file(run_name, ".dtb")
+                self.dump_dtb(run_state, test_args, base_dtb_path)
+                self.overlay_dtb(
+                    run_state, base_dtb_path, self.args.manifest, dtb_path)
+
+            # Execute test in QEMU..
+            exec_args = self.gen_exec_args(test_args, dtb_path=dtb_path)
+            self.exec_logged(run_state, exec_args)
+        except DriverRunException:
+            pass
+
+        return self.finish_run(run_state)
 
 
 class FvpDriver(Driver):
@@ -272,7 +322,8 @@
     def run(self, run_name, test_args):
         run_state = self.start_run(run_name)
 
-        dts_path = self.args.artifacts.create_file(run_name, ".dts")
+        base_dts_path = self.args.artifacts.create_file(run_name, ".base.dts")
+        base_dtb_path = self.args.artifacts.create_file(run_name, ".base.dtb")
         dtb_path = self.args.artifacts.create_file(run_name, ".dtb")
         uart0_log_path = self.args.artifacts.create_file(run_name, ".uart0.log")
         uart1_log_path = self.args.artifacts.create_file(run_name, ".uart1.log")
@@ -283,20 +334,33 @@
         else:
             initrd_end = 0x85000000  # Default value
 
-        # Create a FDT to pass to FVP.
-        self.gen_dts(dts_path, test_args, initrd_start, initrd_end)
-        dtc_args = [ DTC_SCRIPT, "-i", dts_path, "-o", dtb_path ]
-        self.exec_logged(run_state, dtc_args)
+        try:
+            # Create a DT to pass to FVP.
+            self.gen_dts(base_dts_path, test_args, initrd_start, initrd_end)
 
-        # Run FVP.
-        fvp_args = self.gen_fvp_args(
-            initrd_start, uart0_log_path, uart1_log_path, dtb_path)
-        ret_code = self.exec_logged(run_state, fvp_args)
+            # Compile DTS to DTB.
+            dtc_args = [
+                DTC_SCRIPT, "compile", "-i", base_dts_path, "-o", base_dtb_path,
+            ]
+            self.exec_logged(run_state, dtc_args)
+
+            # If manifest DTBO specified, overlay it.
+            if self.args.manifest:
+                self.overlay_dtb(
+                    run_state, base_dtb_path, self.args.manifest, dtb_path)
+            else:
+                dtb_path = base_dtb_path
+
+            # Run FVP.
+            fvp_args = self.gen_fvp_args(
+                initrd_start, uart0_log_path, uart1_log_path, dtb_path)
+            self.exec_logged(run_state, fvp_args)
+        except DriverRunException:
+            pass
 
         # Append UART0 output to main log.
         append_file(run_state.log_path, read_file(uart0_log_path))
-
-        return self.finish_run(run_state, ret_code)
+        return self.finish_run(run_state)
 
 
 # Tuple used to return information about the results of running a set of tests.
@@ -452,9 +516,12 @@
     # Resolve some paths.
     image = os.path.join(args.out, args.image + ".bin")
     initrd = None
+    manifest = None
     image_name = args.image
     if args.initrd:
-        initrd = os.path.join(args.out_initrd, "obj", args.initrd, "initrd.img")
+        initrd_dir = os.path.join(args.out_initrd, "obj", args.initrd)
+        initrd = os.path.join(initrd_dir, "initrd.img")
+        manifest = os.path.join(initrd_dir, "manifest.dtbo")
         image_name += "_" + args.initrd
     vm_args = args.vm_args or ""
 
@@ -462,7 +529,7 @@
     artifacts = ArtifactsManager(os.path.join(args.log, image_name))
 
     # Create a driver for the platform we want to test on.
-    driver_args = DriverArgs(artifacts, image, initrd, vm_args)
+    driver_args = DriverArgs(artifacts, image, initrd, manifest, vm_args)
     if args.fvp:
         driver = FvpDriver(driver_args)
     else:
diff --git a/test/linux/manifest.dts b/test/linux/manifest.dts
index f863c6e..1372663 100644
--- a/test/linux/manifest.dts
+++ b/test/linux/manifest.dts
@@ -15,8 +15,9 @@
  */
 
 /dts-v1/;
+/plugin/;
 
-/ {
+&{/} {
 	hypervisor {
 		compatible = "hafnium,hafnium";
 		vm1 {
diff --git a/test/vmapi/gicv3/manifest.dts b/test/vmapi/gicv3/manifest.dts
index da3cc13..4e3a769 100644
--- a/test/vmapi/gicv3/manifest.dts
+++ b/test/vmapi/gicv3/manifest.dts
@@ -15,8 +15,9 @@
  */
 
 /dts-v1/;
+/plugin/;
 
-/ {
+&{/} {
 	hypervisor {
 		compatible = "hafnium,hafnium";
 		vm1 {
diff --git a/test/vmapi/primary_only/manifest.dts b/test/vmapi/primary_only/manifest.dts
index 11d7edf..3cf090d 100644
--- a/test/vmapi/primary_only/manifest.dts
+++ b/test/vmapi/primary_only/manifest.dts
@@ -15,8 +15,9 @@
  */
 
 /dts-v1/;
+/plugin/;
 
-/ {
+&{/} {
 	hypervisor {
 		compatible = "hafnium,hafnium";
 		vm1 {
diff --git a/test/vmapi/primary_with_secondaries/manifest.dts b/test/vmapi/primary_with_secondaries/manifest.dts
index ac1530e..6460a60 100644
--- a/test/vmapi/primary_with_secondaries/manifest.dts
+++ b/test/vmapi/primary_with_secondaries/manifest.dts
@@ -15,8 +15,9 @@
  */
 
 /dts-v1/;
+/plugin/;
 
-/ {
+&{/} {
 	hypervisor {
 		compatible = "hafnium,hafnium";
 		vm1 {