Build Linux and the Linux driver out of tree

Linux and the Hafnium module have been built inside their source tree so
far. The targets have a hardcoded toolchain but they get built multiple
times, once for each toolchain defined in GN. This can cause the build
to fail when two instances attempt to compile the same file at the same
time. Refactor the build scripts to compile the kernel and the module
in the out/ directory of each corresponding toolchain.

This cleans up the build files a bit but also introduces a new problem.
Out-of-tree kernel modules do not support building out of their folder.
This patch avoids that by copying the source files into
`target_out_dir`.

Test: kokoro/ubuntu/build.sh
Change-Id: I6fc10ebd9623f5f82dd086447b80d935c490765e
diff --git a/build/linux/linux.gni b/build/linux/linux.gni
new file mode 100644
index 0000000..0f096eb
--- /dev/null
+++ b/build/linux/linux.gni
@@ -0,0 +1,105 @@
+# Copyright 2019 The Hafnium Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+template("linux_kernel") {
+  # TODO: target has no "sources"
+
+  # Args to build/make.py to start the Linux build.
+  shared_args = [
+    "--directory",
+    rebase_path(invoker.kernel_dir),
+
+    # TODO: Build with toolchain cc instead of a hardcoded one.
+    "CC=" + rebase_path("//prebuilts/linux-x64/clang/bin/clang"),
+    "ARCH=arm64",
+    "CROSS_COMPILE=aarch64-linux-gnu-",
+
+    # Build out-of-tree in `target_out_dir`.
+    "O=" + rebase_path(target_out_dir),
+
+    # TODO: Remove/replace.
+    "-j24",
+  ]
+
+  # Subtarget which runs `defconfig` and `modules_prepare`. Used by targets
+  # which do not require the whole kernel to have been built.
+  action("${target_name}__defconfig") {
+    script = "//build/make.py"
+    args = shared_args + [
+             "defconfig",
+             "modules_prepare",
+           ]
+
+    # We never use the output but GN requires each target to have one, and for
+    # its timestamp to change after a recompile. Use the .config file.
+    outputs = [
+      "${target_out_dir}/.config",
+    ]
+  }
+
+  action(target_name) {
+    script = "//build/make.py"
+    output_file = "${target_out_dir}/${target_name}.bin"
+    args = shared_args + [
+             "--copy_out_file",
+             rebase_path("${target_out_dir}/arch/arm64/boot/Image"),
+             rebase_path(output_file),
+           ]
+    outputs = [
+      output_file,
+    ]
+    deps = [
+      ":${target_name}__defconfig",
+    ]
+  }
+}
+
+template("linux_kernel_module") {
+  # Out-of-tree modules cannot be built outside of their directory.
+  # So as to avoid parallel builds under different toolchains clashing,
+  # work around by copying source files to `target_out_dir`.
+  copy("${target_name}__copy_source") {
+    forward_variables_from(invoker,
+                           [
+                             "sources",
+                             "testonly",
+                           ])
+    outputs = [
+      "${target_out_dir}/{{source_file_part}}",
+    ]
+  }
+
+  action(target_name) {
+    forward_variables_from(invoker, [ "testonly" ])
+    script = "//build/make.py"
+    args = [
+      "--directory",
+      rebase_path(target_out_dir),
+      "HAFNIUM_PATH=" + rebase_path("//"),
+      "KERNEL_PATH=" + rebase_path(invoker.kernel_src_dir),
+      "O=" +
+          rebase_path(get_label_info(invoker.kernel_target, "target_out_dir")),
+      "CC=" + rebase_path("//prebuilts/linux-x64/clang/bin/clang"),
+      "ARCH=arm64",
+      "CROSS_COMPILE=aarch64-linux-gnu-",
+    ]
+    outputs = [
+      "${target_out_dir}/${invoker.module_name}.ko",
+    ]
+    deps = [
+      ":${target_name}__copy_source",
+      "${invoker.kernel_target}__defconfig",
+    ]
+  }
+}
diff --git a/build/make.py b/build/make.py
index d4cc0d8..75f0b7b 100644
--- a/build/make.py
+++ b/build/make.py
@@ -26,8 +26,8 @@
 def Main():
     parser = argparse.ArgumentParser()
     parser.add_argument("--directory", required=True)
-    parser.add_argument("--out_file", required=True)
-    parser.add_argument("--copy_out_file", required=True)
+    parser.add_argument("--copy_out_file", nargs=2,
+                        help="Copy file after building. Takes two params: <src> <dest>")
     args, make_args = parser.parse_known_args()
 
     os.chdir(args.directory)
@@ -36,7 +36,8 @@
     if status != 0:
         return status
 
-    shutil.copyfile(args.out_file, args.copy_out_file)
+    if args.copy_out_file is not None:
+        shutil.copyfile(args.copy_out_file[0], args.copy_out_file[1])
     return 0
 
 
diff --git a/driver/BUILD.gn b/driver/BUILD.gn
index f6f4119..0fae976 100644
--- a/driver/BUILD.gn
+++ b/driver/BUILD.gn
@@ -12,28 +12,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-action("linux") {
-  script = "//build/make.py"
-  args = [
-    "--directory",
-    rebase_path("linux"),
-    "--out_file",
-    "hafnium.ko",
-    "--copy_out_file",
-    rebase_path("${target_out_dir}/linux/hafnium.ko"),
-    "CC=" + rebase_path("//prebuilts/linux-x64/clang/bin/clang"),
-    "ARCH=arm64",
-    "CROSS_COMPILE=aarch64-linux-gnu-",
-  ]
+import("//build/linux/linux.gni")
+
+linux_kernel_module("linux") {
+  module_name = "hafnium"
+  kernel_target = "//third_party:linux"
+  kernel_src_dir = "//third_party/linux"
   sources = [
     "linux/Makefile",
     "linux/hf_call.S",
     "linux/main.c",
   ]
-  outputs = [
-    "${target_out_dir}/linux/hafnium.ko",
-  ]
-  deps = [
-    "//third_party:linux_defconfig",
-  ]
 }
diff --git a/driver/linux b/driver/linux
index ddc3394..afea42a 160000
--- a/driver/linux
+++ b/driver/linux
@@ -1 +1 @@
-Subproject commit ddc3394d6462cd4e4a33ea3dc7edca7aed0f1ca3
+Subproject commit afea42a6cd9592170fb6fff362e998b3706a7512
diff --git a/test/linux/BUILD.gn b/test/linux/BUILD.gn
index 0c8a9c6..57ea451 100644
--- a/test/linux/BUILD.gn
+++ b/test/linux/BUILD.gn
@@ -27,14 +27,16 @@
 
 linux_initrd("linux_test_initrd") {
   testonly = true
+
+  # Always use the aarch64_linux_clang toolchain to build test_binary
+  test_binary_target = ":test_binary(//build/toolchain:aarch64_linux_clang)"
   sources = [
-    get_label_info(":test_binary(//build/toolchain:aarch64_linux_clang)",
-                   "root_out_dir") + "/test_binary",
-    get_label_info("//driver:linux", "target_out_dir") + "/linux/hafnium.ko",
+    get_label_info(test_binary_target, "root_out_dir") + "/test_binary",
+    get_label_info("//driver:linux", "target_out_dir") + "/hafnium.ko",
   ]
   deps = [
-    ":test_binary(//build/toolchain:aarch64_linux_clang)",
     "//driver:linux",
+    test_binary_target,
   ]
 }
 
diff --git a/third_party/BUILD.gn b/third_party/BUILD.gn
index 52ae5e9..9751f25 100644
--- a/third_party/BUILD.gn
+++ b/third_party/BUILD.gn
@@ -12,6 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import("//build/linux/linux.gni")
+
+# TODO: Having all third_party targets defined in this file means they share
+# the same $target_out_dir folder. Ideally we would like to avoid that.
+
 config("gtest_config") {
   visibility = [ ":gtest" ]
 
@@ -51,46 +56,6 @@
   ]
 }
 
-action("linux_defconfig") {
-  script = "//build/make.py"
-  args = [
-    "--directory",
-    rebase_path("linux"),
-    "--out_file",
-    ".config",
-    "--copy_out_file",
-    rebase_path("${target_out_dir}/.config"),
-    "CC=" + rebase_path("//prebuilts/linux-x64/clang/bin/clang"),
-    "ARCH=arm64",
-    "CROSS_COMPILE=aarch64-linux-gnu-",
-    "-j24",
-    "defconfig",
-    "modules_prepare",
-  ]
-  outputs = [
-    # We don't actually care about this, but GN requires us to have some output.
-    "${target_out_dir}/.config",
-  ]
-}
-
-action("linux") {
-  script = "//build/make.py"
-  args = [
-    "--directory",
-    rebase_path("linux"),
-    "--out_file",
-    "arch/arm64/boot/Image",
-    "--copy_out_file",
-    rebase_path("${target_out_dir}/linux.bin"),
-    "CC=" + rebase_path("//prebuilts/linux-x64/clang/bin/clang"),
-    "ARCH=arm64",
-    "CROSS_COMPILE=aarch64-linux-gnu-",
-    "-j24",
-  ]
-  outputs = [
-    "${target_out_dir}/linux.bin",
-  ]
-  deps = [
-    ":linux_defconfig",
-  ]
+linux_kernel("linux") {
+  kernel_dir = "linux"
 }