[recovery] Add scheme_map config

appmgr no longer boots without a scheme_map, which means recovery was
crashing on startup prior to this CL.

Testing this change requires building out a testing framework for
running QEMU directly because we need to run the recovery system rather
than a normal Fuchsia system.

Change-Id: If640a0886e3d690c90661e6e62879ce997ee7857
diff --git a/build/config/fuchsia/zbi.gni b/build/config/fuchsia/zbi.gni
index 0facf2a..77920e80 100644
--- a/build/config/fuchsia/zbi.gni
+++ b/build/config/fuchsia/zbi.gni
@@ -77,8 +77,9 @@
     forward_variables_from(invoker,
                            [
                              "deps",
-                             "visibility",
+                             "metadata",
                              "testonly",
+                             "visibility",
                            ])
     outputs = [
       output_file,
diff --git a/build/images/assemble_system.gni b/build/images/assemble_system.gni
index c372f1e..e1d5b51 100644
--- a/build/images/assemble_system.gni
+++ b/build/images/assemble_system.gni
@@ -239,6 +239,20 @@
   }
 
   zbi(image_name) {
+    metadata = {
+      # We insert a package barrier because the packages inside this ZBI
+      # shouldn't leak to targets that depend on the ZBI. For example,
+      # suppose we store this ZBI inside a package() that is assembled into
+      # another Fuchsia system. We don't want the parent system to incorporate
+      # the packages from the ZBI into its own package list.
+      package_barrier = []
+      config_package_barrier = []
+
+      if (defined(invoker.metadata)) {
+        forward_variables_from(invoker.metadata, [ "metadata" ])
+      }
+    }
+
     testonly = true
     deps = [
       ":devmgr_config.txt",
diff --git a/build/images/recovery/BUILD.gn b/build/images/recovery/BUILD.gn
index 34f1eaa..b4bfef7 100644
--- a/build/images/recovery/BUILD.gn
+++ b/build/images/recovery/BUILD.gn
@@ -8,6 +8,7 @@
 
 recovery_packages = [
   "//garnet:build-info",
+  "//garnet/bin/appmgr:appmgr_scheme_config",
   "//garnet/bin/appmgr",
   "//garnet/bin/netcfg:config",
   "//garnet/bin/netcfg",
@@ -46,4 +47,8 @@
   base_packages = recovery_packages
   netboot = true
   devmgr_config = [ "virtcon.disable=true" ]
+
+  metadata = {
+    test_runtime_deps = [ "$root_out_dir/${target_name}.zbi" ]
+  }
 }
diff --git a/src/recovery/simulator/BUILD.gn b/src/recovery/simulator/BUILD.gn
index dde3f8f..4ebfd4c 100644
--- a/src/recovery/simulator/BUILD.gn
+++ b/src/recovery/simulator/BUILD.gn
@@ -29,5 +29,6 @@
     deps = [
       ":lib",
     ]
+    non_go_deps = [ "//build/images/recovery($target_toolchain)" ]
   }
 }
diff --git a/src/recovery/simulator/simulator_test.go b/src/recovery/simulator/simulator_test.go
index 12f7797..cf1626b 100644
--- a/src/recovery/simulator/simulator_test.go
+++ b/src/recovery/simulator/simulator_test.go
@@ -5,15 +5,46 @@
 package simulator
 
 import (
+	"os"
+	"path/filepath"
 	"testing"
 
 	"fuchsia.googlesource.com/testing/qemu"
 )
 
+func zbiPath(t *testing.T) string {
+	ex, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+		return ""
+	}
+	exPath := filepath.Dir(ex)
+	return filepath.Join(exPath, "../recovery.zbi")
+}
+
 // TestUnpack checks that we can unpack qemu.
-func TestUnpack(t *testing.T) {
-	err := qemu.Unpack()
+func TestBoot(t *testing.T) {
+	distro, err := qemu.Unpack()
 	if err != nil {
 		t.Fatal(err)
 	}
+	defer distro.Delete()
+
+	arch, err := distro.TargetCPU()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	i := distro.Create(qemu.Params{
+		Arch: arch,
+		ZBI:  zbiPath(t),
+	})
+
+	i.Start()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer i.Kill()
+
+	i.WaitForLogMessage("recovery: started")
 }
diff --git a/src/testing/qemu/BUILD.gn b/src/testing/qemu/BUILD.gn
index 01aa5f4..6f90b6e 100644
--- a/src/testing/qemu/BUILD.gn
+++ b/src/testing/qemu/BUILD.gn
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//build/go/go_library.gni")
+import("//build/config/fuchsia/zircon.gni")
 
 # Currently, we ony support working with the QEMU simulator on Linux.
 if (is_linux) {
@@ -36,8 +37,34 @@
     }
   }
 
+  copy("kernel") {
+    visibility = [ ":*" ]
+
+    kernel_binary = ""
+    if (target_cpu == "arm64") {
+      kernel_binary = "qemu-boot-shim.bin"
+    } else if (target_cpu == "x64") {
+      kernel_binary = "multiboot.bin"
+    } else {
+      assert(false, "CPU not supported")
+    }
+    target_cpu_txt = "$root_out_dir/test_data/qemu/target_cpu.txt"
+
+    sources = [ "$zircon_root_build_dir/$kernel_binary" ]
+    outputs = [ "$root_out_dir/test_data/qemu/$kernel_binary" ]
+
+    write_file(target_cpu_txt, target_cpu)
+
+    metadata = {
+      test_runtime_deps = [
+        outputs[0],
+        target_cpu_txt,
+      ]
+    }
+  }
+
   go_library("qemu") {
     name = "fuchsia.googlesource.com/testing/qemu"
-    non_go_deps = [ ":archive" ]
+    non_go_deps = [ ":archive", ":kernel" ]
   }
 }
diff --git a/src/testing/qemu/qemu.go b/src/testing/qemu/qemu.go
index 43abb0d..5697280 100644
--- a/src/testing/qemu/qemu.go
+++ b/src/testing/qemu/qemu.go
@@ -5,19 +5,215 @@
 package qemu
 
 import (
+	"archive/tar"
+	"bufio"
+	"compress/gzip"
+	"fmt"
+	"io"
+	"io/ioutil"
 	"os"
+	"os/exec"
 	"path/filepath"
+	"strings"
 )
 
-// Unpack the QEMU instance.
-func Unpack() error {
-	ex, err := os.Executable()
+// Untar untars a tar.gz file into a directory.
+func untar(dst string, src string) error {
+	f, err := os.Open(src)
 	if err != nil {
 		return err
 	}
+	defer f.Close()
+
+	gz, err := gzip.NewReader(f)
+	if err != nil {
+		return err
+	}
+	defer gz.Close()
+
+	tr := tar.NewReader(gz)
+
+	for {
+		header, err := tr.Next()
+		if err == io.EOF {
+			return nil
+		} else if err != nil {
+			return err
+		}
+
+		path := filepath.Join(dst, header.Name)
+		info := header.FileInfo()
+		if info.IsDir() {
+			if err := os.MkdirAll(path, info.Mode()); err != nil {
+				return err
+			}
+		} else {
+			if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+				return err
+			}
+
+			f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode())
+			if err != nil {
+				return err
+			}
+
+			if _, err := io.Copy(f, tr); err != nil {
+				f.Close()
+				return err
+			}
+
+			f.Close()
+		}
+	}
+}
+
+// Distribution is a collection of QEMU-related artifacts.
+type Distribution struct {
+	exPath       string
+	unpackedPath string
+}
+
+type Arch int
+
+const (
+	X64 Arch = iota
+	Arm64
+)
+
+// Params describes how to run a QEMU instance.
+type Params struct {
+	Arch Arch
+	ZBI  string
+}
+
+type Instance struct {
+	cmd    *exec.Cmd
+	stdin  *bufio.Writer
+	stdout *bufio.Reader
+	stderr *bufio.Reader
+}
+
+// Unpack unpacks the QEMU distribution.
+func Unpack() (*Distribution, error) {
+	ex, err := os.Executable()
+	if err != nil {
+		return nil, err
+	}
 	exPath := filepath.Dir(ex)
 	archivePath := filepath.Join(exPath, "test_data/qemu/qemu.tar.gz")
 
-	_, err = os.Stat(archivePath)
+	unpackedPath, err := ioutil.TempDir("", "qemu-distro")
+	if err != nil {
+		return nil, err
+	}
+
+	err = untar(unpackedPath, archivePath)
+	if err != nil {
+		os.RemoveAll(unpackedPath)
+		return nil, err
+	}
+
+	return &Distribution{exPath: exPath, unpackedPath: unpackedPath}, nil
+}
+
+// Delete removes the QEMU-related artifacts.
+func (d *Distribution) Delete() {
+	os.RemoveAll(d.unpackedPath)
+}
+
+func (d *Distribution) systemPath(arch Arch) string {
+	switch arch {
+	case X64:
+		return filepath.Join(d.unpackedPath, "bin/qemu-system-x86_64")
+	case Arm64:
+		return filepath.Join(d.unpackedPath, "bin/qemu-system-aarch64")
+	}
+	return ""
+}
+
+func (d *Distribution) kernelPath(arch Arch) string {
+	switch arch {
+	case X64:
+		return filepath.Join(d.exPath, "test_data/qemu/multiboot.bin")
+	case Arm64:
+		return filepath.Join(d.exPath, "test_data/qemu/qemu-boot-shim.bin")
+	}
+	return ""
+}
+
+// TargetCPU returs the target CPU used by the build that produced this library.
+func (d *Distribution) TargetCPU() (Arch, error) {
+	path := filepath.Join(d.exPath, "test_data/qemu/target_cpu.txt")
+	bytes, err := ioutil.ReadFile(path)
+	if err != nil {
+		return X64, err
+	}
+	name := string(bytes)
+	switch name {
+	case "x64":
+		return X64, nil
+	case "arm64":
+		return Arm64, nil
+	}
+	return X64, fmt.Errorf("unknown target CPU: %s", name)
+}
+
+// Create creates an instance of QEMU with the given parameters.
+func (d *Distribution) Create(params Params) *Instance {
+	path := d.systemPath(params.Arch)
+	args := []string{}
+	args = append(args, "-kernel", d.kernelPath(params.Arch), "-initrd", params.ZBI)
+	args = append(args, "-m", "2048", "-nographic", "-net", "none", "-smp", "4,threads=2",
+		"-machine", "q35", "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",
+		"-cpu", "Haswell,+smap,-check,-fsgsbase")
+	cmdline := "kernel.serial=legacy kernel.entropy-mixin=1420bb81dc0396b37cc2d0aa31bb2785dadaf9473d0780ecee1751afb5867564 kernel.halt-on-panic=true"
+	args = append(args, "-append", cmdline)
+	return &Instance{
+		cmd: exec.Command(path, args...),
+	}
+}
+
+// Start the QEMU instance.
+func (i *Instance) Start() error {
+	stdin, err := i.cmd.StdinPipe()
+	if err != nil {
+		return err
+	}
+	stdout, err := i.cmd.StdoutPipe()
+	if err != nil {
+		return err
+	}
+	stderr, err := i.cmd.StderrPipe()
+	if err != nil {
+		return err
+	}
+	i.stdin = bufio.NewWriter(stdin)
+	i.stdout = bufio.NewReader(stdout)
+	i.stderr = bufio.NewReader(stderr)
+	return i.cmd.Start()
+}
+
+// Kill terminates the QEMU instance.
+func (i *Instance) Kill() error {
+	return i.cmd.Process.Kill()
+}
+
+// RunCommand runs the given command in the serial console for the QEMU instance.
+func (i *Instance) RunCommand(cmd string) error {
+	_, err := i.stdin.WriteString(fmt.Sprintf("%s\n", cmd))
 	return err
 }
+
+// WaitForLogMessage reads log messages from the QEMU instance until it reads
+// a message that contains the given string.
+func (i *Instance) WaitForLogMessage(msg string) error {
+	for {
+		line, err := i.stdout.ReadString('\n')
+		if err != nil {
+			return err
+		}
+		if strings.Contains(line, msg) {
+			return nil
+		}
+	}
+}