core: build initramfs using generic initramfs rule

This chips away at three different things:
 - it brings us closer to hermetic and cross-platform builds by not
   depending on genrule/shell and lz4-the-tool
 - it generalizes initramfs building (allowing for more than one to be
   built, if necessary)
 - sets the stage to use Bazel transitions [1] to force all included Go
   binaries to be built in pure/static mode while allowing host Go
   binaries to use cgo/dynamic linking if necessary, and hopefully also
   allowing us to get rid of some BUILD patches that set pure='on' in
   go_binary calls (notably needed in Cilium and some existing
   third_party dependencies).

[1] - https://docs.bazel.build/versions/master/skylark/config.html#user-defined-transitions

Test Plan: build machinery change, covered by existing tests

X-Origin-Diff: phab/D554
GitOrigin-RevId: a5561eb5ca16e6529b9a4a2b98352f579c424222
diff --git a/WORKSPACE b/WORKSPACE
index 8d22468..b002958 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -32,8 +32,8 @@
     # Pin slightly above 0.23.0 (and 0.23.1 prerelease at time of writing) to pull in this commit.
     # This fixes https://github.com/bazelbuild/rules_go/issues/2499, which started manifesting at 0.22.4.
     name = "io_bazel_rules_go",
-    strip_prefix = "rules_go-c07100d793fc0cdb20bc4f0361c1d53987ba259b",
     sha256 = "89501e6e6ae6308e82239f0c8e53dceaa428ad8de471a7f8be8b99a1717bb7d8",
+    strip_prefix = "rules_go-c07100d793fc0cdb20bc4f0361c1d53987ba259b",
     urls = [
         "https://github.com/bazelbuild/rules_go/archive/c07100d793fc0cdb20bc4f0361c1d53987ba259b.zip",
     ],
@@ -177,3 +177,13 @@
     sha256 = "adf770dfd574a0d6026bfaa270cb6879b063957177a991d453ff1d302c02081f",
     urls = ["https://curl.haxx.se/ca/cacert-2020-01-01.pem"],
 )
+
+# lz4, the library and the tool.
+http_archive(
+    name = "com_github_lz4_lz4",
+    patch_args = ["-p1"],
+    patches = ["//third_party/lz4:build.patch"],
+    sha256 = "658ba6191fa44c92280d4aa2c271b0f4fbc0e34d249578dd05e50e76d0e5efcc",
+    strip_prefix = "lz4-1.9.2",
+    urls = ["https://github.com/lz4/lz4/archive/v1.9.2.tar.gz"],
+)
diff --git a/build/savestdout/BUILD.bazel b/build/savestdout/BUILD.bazel
new file mode 100644
index 0000000..7208c6d
--- /dev/null
+++ b/build/savestdout/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["savestdout.go"],
+    importpath = "git.monogon.dev/source/nexantic.git/build/savestdout",
+    visibility = ["//visibility:private"],
+)
+
+go_binary(
+    name = "savestdout",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/build/savestdout/README.md b/build/savestdout/README.md
new file mode 100644
index 0000000..a233620
--- /dev/null
+++ b/build/savestdout/README.md
@@ -0,0 +1,22 @@
+savestdout
+==========
+
+`savestdout` is a small tool to save the stdout of a command to a file, without using
+a shell.
+
+It was made to be used in Bazel rule definitions that want to run a command and save
+its output to stdout without going through ctx.actions.run_shell.
+
+Once [bazelbuild/bazel/issues/5511](https://github.com/bazelbuild/bazel/issues/5511)
+gets fixed, rules that need this behaviour can start using native Bazel functionality
+instead, and this tool should be deleted.
+
+Usage
+-----
+
+Command line usage:
+
+    bazel build //build/savestdout
+    bazel run bazel-bin/build/savestdout/*/savestdout /tmp/foo ps aux
+
+For an example of use in rules, see `smalltown_initramfs` in `//code/def.bzl`.
diff --git a/build/savestdout/savestdout.go b/build/savestdout/savestdout.go
new file mode 100644
index 0000000..a0fb709
--- /dev/null
+++ b/build/savestdout/savestdout.go
@@ -0,0 +1,51 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// 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
+//
+//     http://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.
+
+package main
+
+import (
+	"log"
+	"os"
+	"os/exec"
+)
+
+func main() {
+	if len(os.Args) < 3 {
+		log.Fatalf("Usage: %s output_file program <args...>", os.Args[0])
+	}
+
+	f, err := os.Create(os.Args[1])
+	if err != nil {
+		log.Fatalf("Create(%q): %v", os.Args[1], err)
+	}
+	defer f.Close()
+
+	args := os.Args[3:]
+	cmd := exec.Command(os.Args[2], args...)
+	cmd.Stderr = os.Stderr
+	cmd.Stdout = f
+
+	err = cmd.Run()
+	if err == nil {
+		return
+	}
+
+	if e, ok := err.(*exec.ExitError); ok {
+		os.Exit(e.ExitCode())
+	}
+
+	log.Fatalf("Could not start command: %v", err)
+}
diff --git a/core/BUILD b/core/BUILD
index 03a5153..1229388 100644
--- a/core/BUILD
+++ b/core/BUILD
@@ -1,67 +1,40 @@
-genrule(
+load("//core/build:def.bzl", "smalltown_initramfs")
+
+smalltown_initramfs(
     name = "initramfs",
-    srcs = [
-        "//core/cmd/init",
-        "//core/cmd/kube",
-        "//third_party/xfsprogs:mkfs.xfs",
-        "@io_k8s_kubernetes//cmd/kubelet:_kubelet-pure",
-        "@com_github_containerd_containerd//cmd/containerd",
-        "@com_github_containerd_containerd//cmd/containerd-shim-runc-v2",
-        "@com_github_containernetworking_plugins//plugins/main/loopback",
-        "@com_github_containernetworking_plugins//plugins/main/ptp",
-        "@com_github_containernetworking_plugins//plugins/ipam/host-local",
-        "@com_github_google_gvisor//runsc",
-        "@com_github_google_gvisor_containerd_shim//cmd/containerd-shim-runsc-v1",
-        "//core/internal/containerd:ptp.json",
-        "//core/internal/containerd:loopback.json",
-        "//core/internal/containerd:config.toml",
-        "//core/internal/containerd:runsc.toml",
-        "@cacerts//file",
-        ":os-release-info",
+    extra_dirs = [
+        "/kubernetes/conf/flexvolume-plugins",
+        "/containerd/run",
     ],
-    outs = [
-        "initramfs.cpio.lz4",
-    ],
-    cmd = """
-    $(location @linux//:gen_init_cpio) - <<- 'EOF' | lz4 -l > \"$@\" 
-dir /dev 0755 0 0
-nod /dev/console 0600 0 0 c 5 1
-nod /dev/null 0644 0 0 c 1 3
-nod /dev/kmsg 0644 0 0 c 1 11
-nod /dev/ptmx 0644 0 0 c 5 2
-file /init $(location //core/cmd/init) 0755 0 0
-dir /etc 0755 0 0
-file /etc/os-release $(location :os-release-info) 0644 0 0
-dir /etc/ssl 0755 0 0
-file /etc/ssl/cert.pem $(location @cacerts//file) 0444 0 0
-dir /bin 0755 0 0
-file /bin/mkfs.xfs $(location //third_party/xfsprogs:mkfs.xfs) 0755 0 0
-dir /kubernetes 0755 0 0
-dir /kubernetes/bin 0755 0 0
-file /kubernetes/bin/kube $(location //core/cmd/kube) 0755 0 0
-dir /kubernetes/conf 0755 0 0
-dir /kubernetes/conf/flexvolume-plugins 0755 0 0
-dir /containerd 0755 0 0
-dir /containerd/bin 0755 0 0
-file /containerd/bin/containerd $(location @com_github_containerd_containerd//cmd/containerd) 0755 0 0
-file /containerd/bin/containerd-shim-runsc-v1 $(location @com_github_google_gvisor_containerd_shim//cmd/containerd-shim-runsc-v1) 0755 0 0
-file /containerd/bin/runsc $(location @com_github_google_gvisor//runsc) 0755 0 0
-dir /containerd/bin/cni 0755 0 0
-file /containerd/bin/cni/loopback $(location @com_github_containernetworking_plugins//plugins/main/loopback) 0755 0 0
-file /containerd/bin/cni/ptp $(location @com_github_containernetworking_plugins//plugins/main/ptp) 0755 0 0
-file /containerd/bin/cni/host-local $(location @com_github_containernetworking_plugins//plugins/ipam/host-local) 0755 0 0
-dir /containerd/run 0755 0 0
-dir /containerd/conf 0755 0 0
-dir /containerd/conf/cni 0755 0 0
-file /containerd/conf/cni/10-ptp.conf $(location //core/internal/containerd:ptp.json) 0444 0 0
-file /containerd/conf/cni/99-loopback.conf $(location //core/internal/containerd:loopback.json) 0444 0 0
-file /containerd/conf/config.toml $(location //core/internal/containerd:config.toml) 0444 0 0
-file /containerd/conf/runsc.toml $(location //core/internal/containerd:runsc.toml) 0444 0 0
-EOF
-    """,
-    tools = [
-        "@linux//:gen_init_cpio",
-    ],
+    files = {
+        "//core/cmd/init": "/init",
+        "//third_party/xfsprogs:mkfs.xfs": "/bin/mkfs.xfs",
+
+        # CA Certificate bundle & os-release
+        "@cacerts//file": "/etc/ssl/cert.pem",
+        ":os-release-info": "/etc/os-release",
+
+        # Hyperkube
+        "//core/cmd/kube": "/kubernetes/bin/kube",
+
+        # runsc/gVisor
+        "@com_github_google_gvisor//runsc": "/containerd/bin/runsc",
+        "@com_github_google_gvisor_containerd_shim//cmd/containerd-shim-runsc-v1": "/containerd/bin/containerd-shim-runsc-v1",
+
+        # Containerd
+        "@com_github_containerd_containerd//cmd/containerd": "/containerd/bin/containerd",
+
+        # Containerd config files
+        "//core/internal/containerd:runsc.toml": "/containerd/conf/runsc.toml",
+        "//core/internal/containerd:config.toml": "/containerd/conf/config.toml",
+        "//core/internal/containerd:loopback.json": "/containerd/conf/cni/99-loopback.conf",
+        "//core/internal/containerd:ptp.json": "/containerd/conf/cni/10-ptp.conf",
+
+        # CNI Plugins
+        "@com_github_containernetworking_plugins//plugins/main/loopback": "/containerd/bin/cni/loopback",
+        "@com_github_containernetworking_plugins//plugins/main/ptp": "/containerd/bin/cni/ptp",
+        "@com_github_containernetworking_plugins//plugins/ipam/host-local": "/containerd/bin/cni/host-local",
+    },
 )
 
 genrule(
diff --git a/core/build/BUILD b/core/build/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/build/BUILD
diff --git a/core/build/def.bzl b/core/build/def.bzl
new file mode 100644
index 0000000..69994fc
--- /dev/null
+++ b/core/build/def.bzl
@@ -0,0 +1,172 @@
+#  Copyright 2020 The Monogon Project Authors.
+#
+#  SPDX-License-Identifier: Apache-2.0
+#
+#  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
+#
+#      http://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.
+
+def _smalltown_initramfs_impl(ctx):
+    """
+    Generate an lz4-compressed initramfs based on a label/file list.
+    """
+
+    # Generate config file for gen_init_cpio that describes the initramfs to build.
+    cpio_list_name = ctx.label.name + ".cpio_list"
+    cpio_list = ctx.actions.declare_file(cpio_list_name)
+
+    # Start out with some standard initramfs device files.
+    cpio_list_content = [
+        "dir /dev 0755 0 0",
+        "nod /dev/console 0600 0 0 c 5 1",
+        "nod /dev/null 0644 0 0 c 1 3",
+        "nod /dev/kmsg 0644 0 0 c 1 11",
+        "nod /dev/ptmx 0644 0 0 c 5 2",
+    ]
+
+    # Find all directories that need to be created.
+    directories_needed = []
+    for _, p in ctx.attr.files.items():
+        if not p.startswith("/"):
+            fail("file {} invalid: must begin with /".format(p))
+
+        # Get all intermediate directories on path to file
+        parts = p.split("/")[1:-1]
+        directories_needed.append(parts)
+
+    # Extend with extra directories defined by user.
+    for p in ctx.attr.extra_dirs:
+        if not p.startswith("/"):
+            fail("directory {} invalid: must begin with /".format(p))
+
+        parts = p.split("/")[1:]
+        directories_needed.append(parts)
+
+    directories = []
+    for parts in directories_needed:
+        # Turn directory parts [usr, local, bin] into successive subpaths [/usr, /usr/local, /usr/local/bin].
+        last = ""
+        for part in parts:
+            last += "/" + part
+
+            # TODO(q3k): this is slow - this should be a set instead, but starlark doesn't implement them.
+            # For the amount of files we're dealing with this doesn't matter, but all stars are pointing towards this
+            # becoming accidentally quadratic at some point in the future.
+            if last not in directories:
+                directories.append(last)
+
+    # Append instructions to create directories.
+    # Serendipitously, the directories should already be in the right order due to us not using a set to create the
+    # list. They might not be in an elegant order (ie, if files [/foo/one/one, /bar, /foo/two/two] are request, the
+    # order will be [/foo, /foo/one, /bar, /foo/two]), but that's fine.
+    for d in directories:
+        cpio_list_content.append("dir {} 0755 0 0".format(d))
+
+    # Append instructions to add files.
+    inputs = []
+    for label, p in ctx.attr.files.items():
+        # Figure out if this is an executable.
+        is_executable = True
+
+        di = label[DefaultInfo]
+        if di.files_to_run.executable == None:
+            # Generated non-executable files will have DefaultInfo.files_to_run.executable == None
+            is_executable = False
+        elif di.files_to_run.executable.is_source:
+            # Source files will have executable.is_source == True
+            is_executable = False
+
+        # Ensure only single output is declared.
+        # If you hit this error, figure out a better logic to find what file you need, maybe looking at providers other
+        # than DefaultInfo.
+        files = di.files.to_list()
+        if len(files) > 1:
+            fail("file {} has more than one output: {}", p, files)
+        src = files[0]
+        inputs.append(src)
+
+        mode = "0755" if is_executable else "0444"
+
+        cpio_list_content.append("file {} {} {} 0 0".format(p, src.path, mode))
+
+    # Write cpio_list.
+    ctx.actions.write(cpio_list, "\n".join(cpio_list_content))
+
+    gen_init_cpio = ctx.executable._gen_init_cpio
+    savestdout = ctx.executable._savestdout
+    lz4 = ctx.executable._lz4
+
+    # Generate 'raw' (uncompressed) initramfs
+    initramfs_raw_name = ctx.label.name
+    initramfs_raw = ctx.actions.declare_file(initramfs_raw_name)
+    ctx.actions.run(
+        outputs = [initramfs_raw],
+        inputs = [cpio_list] + inputs,
+        tools = [savestdout, gen_init_cpio],
+        executable = savestdout,
+        arguments = [initramfs_raw.path, gen_init_cpio.path, cpio_list.path],
+    )
+
+    # Compress raw initramfs using lz4c.
+    initramfs_name = ctx.label.name + ".lz4"
+    initramfs = ctx.actions.declare_file(initramfs_name)
+    ctx.actions.run(
+        outputs = [initramfs],
+        inputs = [initramfs_raw],
+        tools = [savestdout, lz4],
+        executable = lz4.path,
+        arguments = ["-l", initramfs_raw.path, initramfs.path],
+    )
+
+    return [DefaultInfo(files = depset([initramfs]))]
+
+smalltown_initramfs = rule(
+    implementation = _smalltown_initramfs_impl,
+    doc = """
+        Build a Smalltown initramfs. The initramfs will contain a basic /dev directory and all the files specified by the
+        `files` attribute. Executable files will have their permissions set to 0755, non-executable files will have
+        their permissions set to 0444. All parent directories will be created with 0755 permissions.
+    """,
+    attrs = {
+        "files": attr.label_keyed_string_dict(
+            mandatory = True,
+            allow_files = True,
+            doc = """
+                Dictionary of Labels to String, placing a given Label's output file in the initramfs at the location
+                specified by the String value. The specified labels must only have a single output.
+            """,
+        ),
+        "extra_dirs": attr.string_list(
+            default = [],
+            doc = """
+                Extra directories to create. These will be created in addition to all the directories required to
+                contain the files specified in the `files` attribute.
+            """,
+        ),
+
+        # Tools, implicit dependencies.
+        "_gen_init_cpio": attr.label(
+            default = Label("@linux//:gen_init_cpio"),
+            executable = True,
+            cfg = "host",
+        ),
+        "_lz4": attr.label(
+            default = Label("@com_github_lz4_lz4//programs:lz4"),
+            executable = True,
+            cfg = "host",
+        ),
+        "_savestdout": attr.label(
+            default = Label("//build/savestdout"),
+            executable = True,
+            cfg = "host",
+        ),
+    },
+)
diff --git a/third_party/go/repositories.bzl b/third_party/go/repositories.bzl
index 32d2ffe..f5c515c 100644
--- a/third_party/go/repositories.bzl
+++ b/third_party/go/repositories.bzl
@@ -1779,15 +1779,3 @@
         version = "v0.0.0-20160121211510-db5cfe13f5cc",
         sum = "h1:MksmcCZQWAQJCTA5T0jgI/0sJ51AVm4Z41MrmfczEoc=",
     )
-    go_repository(
-        name = "com_github_container_storage_interface_spec",
-        importpath = "github.com/container-storage-interface/spec",
-        sum = "h1:bD9KIVgaVKKkQ/UbVUY9kCaH/CJbhNxe0eeB4JeJV2s=",
-        version = "v1.2.0",
-    )
-    go_repository(
-        name = "io_k8s_sigs_structured_merge_diff_v3",
-        importpath = "sigs.k8s.io/structured-merge-diff/v3",
-        sum = "h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=",
-        version = "v3.0.0",
-    )
diff --git a/third_party/lz4/BUILD b/third_party/lz4/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/third_party/lz4/BUILD
diff --git a/third_party/lz4/build.patch b/third_party/lz4/build.patch
new file mode 100644
index 0000000..0415c87
--- /dev/null
+++ b/third_party/lz4/build.patch
@@ -0,0 +1,69 @@
+Copyright 2020 The Monogon Project 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
+
+  http://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.
+
+
+diff -urN com_github_lz4_lz4.orig/lib/BUILD com_github_lz4_lz4/lib/BUILD
+--- com_github_lz4_lz4.orig/lib/BUILD	1970-01-01 01:00:00.000000000 +0100
++++ com_github_lz4_lz4/lib/BUILD	2020-06-05 22:00:01.056028668 +0200
+@@ -0,0 +1,19 @@
++cc_library(
++    name = "lib",
++    srcs = [
++        "lz4frame.c",
++        "lz4.c",
++        "lz4hc.c",
++        "xxhash.c",
++    ],
++    hdrs = [
++        # Yes, this is also a header. lib/lz4hc.c includes it.
++        "lz4.c",
++        "lz4.h",
++        "lz4frame.h",
++        "lz4hc.h",
++        "xxhash.h",
++    ],
++    strip_include_prefix = "//lib",
++    visibility = ["//visibility:public"],
++)
+diff -urN com_github_lz4_lz4.orig/programs/BUILD com_github_lz4_lz4/programs/BUILD
+--- com_github_lz4_lz4.orig/programs/BUILD	1970-01-01 01:00:00.000000000 +0100
++++ com_github_lz4_lz4/programs/BUILD	2020-06-05 21:59:06.233821791 +0200
+@@ -0,0 +1,22 @@
++cc_binary(
++    name = "lz4",
++    srcs = [
++        "lz4cli.c",
++
++        "lz4io.h",
++        "lz4io.c",
++
++        "bench.h",
++        "bench.c",
++
++        "datagen.h",
++        "datagen.c",
++
++        "platform.h",
++        "util.h",
++    ],
++    deps = [
++        "//lib",
++    ],
++    visibility = ["//visibility:public"],
++)
+diff -urN com_github_lz4_lz4.orig/WORKSPACE com_github_lz4_lz4/WORKSPACE
+--- com_github_lz4_lz4.orig/WORKSPACE	1970-01-01 01:00:00.000000000 +0100
++++ com_github_lz4_lz4/WORKSPACE	2020-06-05 21:50:45.128930780 +0200
+@@ -0,0 +1 @@
++workspace(name = "com_github_lz4_lz4")