o/t/ktest: switch to proper rule

This was previously implemented as a macro, lacking proper transitions.
Reimplement it as a proper test rule.

Change-Id: I237a2fcc29ea6dfbb294ce6313c9ff457def12b8
Reviewed-on: https://review.monogon.dev/c/monogon/+/3722
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/go/net/psample/BUILD.bazel b/go/net/psample/BUILD.bazel
index 1634ba9..9eb3378 100644
--- a/go/net/psample/BUILD.bazel
+++ b/go/net/psample/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "psample",
@@ -24,6 +24,7 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     tester = ":psample_test",
 )
diff --git a/metropolis/node/core/clusternet/BUILD.bazel b/metropolis/node/core/clusternet/BUILD.bazel
index 5ab1d8a..7ee8aa1 100644
--- a/metropolis/node/core/clusternet/BUILD.bazel
+++ b/metropolis/node/core/clusternet/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "clusternet",
@@ -45,7 +45,8 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     cmdline = "ramdisk_size=128",
     tester = ":clusternet_test",
 )
diff --git a/metropolis/node/core/localstorage/crypt/BUILD.bazel b/metropolis/node/core/localstorage/crypt/BUILD.bazel
index 39b6a53..094fd07 100644
--- a/metropolis/node/core/localstorage/crypt/BUILD.bazel
+++ b/metropolis/node/core/localstorage/crypt/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "crypt",
@@ -31,7 +31,8 @@
     embed = [":crypt"],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     cmdline = "ramdisk_size=4096",
     tester = ":crypt_test",
 )
diff --git a/metropolis/node/core/network/dhcp4c/callback/BUILD.bazel b/metropolis/node/core/network/dhcp4c/callback/BUILD.bazel
index b6659ba..7e9827d 100644
--- a/metropolis/node/core/network/dhcp4c/callback/BUILD.bazel
+++ b/metropolis/node/core/network/dhcp4c/callback/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "callback",
@@ -31,6 +31,7 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     tester = ":callback_test",
 )
diff --git a/metropolis/test/lacp/BUILD.bazel b/metropolis/test/lacp/BUILD.bazel
index b5a0f7a..9c18c2a 100644
--- a/metropolis/test/lacp/BUILD.bazel
+++ b/metropolis/test/lacp/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_test(
     name = "lacptest_test",
@@ -10,7 +10,8 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     tester = ":lacptest_test",
 )
 
diff --git a/osbase/blockdev/BUILD.bazel b/osbase/blockdev/BUILD.bazel
index a720d80..f9a8534 100644
--- a/osbase/blockdev/BUILD.bazel
+++ b/osbase/blockdev/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "blockdev",
@@ -50,6 +50,7 @@
     }),
 )
 
-ktest(
+k_test(
+    name = "ktest",
     tester = ":blockdev_test",
 )
diff --git a/osbase/build/def.bzl b/osbase/build/def.bzl
index 57a7dab..73752f7 100644
--- a/osbase/build/def.bzl
+++ b/osbase/build/def.bzl
@@ -49,9 +49,9 @@
     },
 )
 
-def _fsspec_core_impl(ctx, tool, output_file):
+def fsspec_core_impl(ctx, tool, output_file, extra_files = [], extra_fsspecs = []):
     """
-    _fsspec_core_impl implements the core of an fsspec-based rule. It takes
+    fsspec_core_impl implements the core of an fsspec-based rule. It takes
     input from the `files`,`files_cc`, `symlinks` and `fsspecs` attributes
     and calls `tool` with the `-out` parameter pointing to `output_file`
     and paths to all fsspecs as positional arguments.
@@ -61,7 +61,7 @@
 
     fs_files = []
     inputs = []
-    for label, p in ctx.attr.files.items() + ctx.attr.files_cc.items():
+    for label, p in ctx.attr.files.items() + ctx.attr.files_cc.items() + extra_files:
         if not p.startswith("/"):
             fail("file {} invalid: must begin with /".format(p))
 
@@ -97,7 +97,7 @@
 
     extra_specs = []
 
-    for fsspec in ctx.attr.fsspecs:
+    for fsspec in ctx.attr.fsspecs + extra_fsspecs:
         if FSSpecInfo in fsspec:
             fsspec_info = fsspec[FSSpecInfo]
             extra_specs.append(fsspec_info.spec)
@@ -121,7 +121,7 @@
     initramfs_name = ctx.label.name + ".cpio.zst"
     initramfs = ctx.actions.declare_file(initramfs_name)
 
-    _fsspec_core_impl(ctx, ctx.executable._mkcpio, initramfs)
+    fsspec_core_impl(ctx, ctx.executable._mkcpio, initramfs)
 
     # TODO(q3k): Document why this is needed
     return [DefaultInfo(runfiles = ctx.runfiles(files = [initramfs]), files = depset([initramfs]))]
@@ -184,7 +184,7 @@
     fs_name = ctx.label.name + ".img"
     fs_out = ctx.actions.declare_file(fs_name)
 
-    _fsspec_core_impl(ctx, ctx.executable._mkerofs, fs_out)
+    fsspec_core_impl(ctx, ctx.executable._mkerofs, fs_out)
 
     return [DefaultInfo(files = depset([fs_out]))]
 
diff --git a/osbase/erofs/BUILD.bazel b/osbase/erofs/BUILD.bazel
index d48eb1f..52f960c 100644
--- a/osbase/erofs/BUILD.bazel
+++ b/osbase/erofs/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "erofs",
@@ -30,7 +30,8 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     cmdline = "ramdisk_size=128",
     tester = ":erofs_test",
 )
diff --git a/osbase/fat32/BUILD.bazel b/osbase/fat32/BUILD.bazel
index 4942844..1e0e909 100644
--- a/osbase/fat32/BUILD.bazel
+++ b/osbase/fat32/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "fat32",
@@ -35,7 +35,8 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     cmdline = "ramdisk_size=266240",
     tester = ":fat32_test",
 )
diff --git a/osbase/fsquota/BUILD.bazel b/osbase/fsquota/BUILD.bazel
index 7c62fd7..75a3133 100644
--- a/osbase/fsquota/BUILD.bazel
+++ b/osbase/fsquota/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "fsquota",
@@ -23,7 +23,8 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     cmdline = "ramdisk_size=51200",
     files_cc = {
         "@xfsprogs//:mkfs": "/mkfs.xfs",
diff --git a/osbase/gpt/BUILD.bazel b/osbase/gpt/BUILD.bazel
index 67a54ee..6c561d4 100644
--- a/osbase/gpt/BUILD.bazel
+++ b/osbase/gpt/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "gpt",
@@ -31,7 +31,8 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     cmdline = "ramdisk_size=4096",
     tester = ":gpt_test",
 )
diff --git a/osbase/kmod/BUILD.bazel b/osbase/kmod/BUILD.bazel
index 4bf0fb5..ce37542 100644
--- a/osbase/kmod/BUILD.bazel
+++ b/osbase/kmod/BUILD.bazel
@@ -1,6 +1,6 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 load("//osbase/build/fwprune:def.bzl", "fsspec_linux_firmware")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "kmod",
@@ -41,7 +41,8 @@
     metadata = "@linux-firmware//:metadata",
 )
 
-ktest(
+k_test(
+    name = "ktest",
     fsspecs = [
         ":firmware",
     ],
diff --git a/osbase/loop/BUILD.bazel b/osbase/loop/BUILD.bazel
index cc52c8d..eca8529 100644
--- a/osbase/loop/BUILD.bazel
+++ b/osbase/loop/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "loop",
@@ -20,6 +20,7 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     tester = ":loop_test",
 )
diff --git a/osbase/test/ktest/ktest.bzl b/osbase/test/ktest/ktest.bzl
index 614832d..42e6b60 100644
--- a/osbase/test/ktest/ktest.bzl
+++ b/osbase/test/ktest/ktest.bzl
@@ -18,41 +18,118 @@
 Ktest provides a macro to run tests under a normal Metropolis node kernel
 """
 
-load("//osbase/build:def.bzl", "node_initramfs")
+load("//osbase/build:def.bzl", "FSSpecInfo", "build_pure_transition", "build_static_transition", "fsspec_core_impl")
 
-def _dict_union(x, y):
-    z = {}
-    z.update(x)
-    z.update(y)
-    return z
+_KTEST_SCRIPT = """
+#!/usr/bin/env bash
 
-def ktest(tester, cmdline = "", files = {}, fsspecs = [], files_cc = {}):
-    node_initramfs(
-        name = "test_initramfs",
-        fsspecs = [
-            "//osbase/build:earlydev.fsspec",
-        ] + fsspecs,
-        files = _dict_union({
-            "//osbase/test/ktest/init": "/init",
-            tester: "/tester",
-        }, files),
-        files_cc = files_cc,
-        testonly = True,
+exec "{ktest}" -initrd-path "{initrd}" -kernel-path "{kernel}" -cmdline "{cmdline}"
+"""
+
+def _ktest_impl(ctx):
+    initramfs_name = ctx.label.name + ".cpio.zst"
+    initramfs = ctx.actions.declare_file(initramfs_name)
+
+    fsspec_core_impl(ctx, ctx.executable._mkcpio, initramfs, [(ctx.attr._ktest_init[0], "/init"), (ctx.attr.tester[0], "/tester")], [ctx.attr._earlydev])
+
+    script_file = ctx.actions.declare_file(ctx.label.name + ".sh")
+
+    ctx.actions.write(
+        output = script_file,
+        content = _KTEST_SCRIPT.format(
+            ktest = ctx.executable._ktest.short_path,
+            initrd = initramfs.short_path,
+            kernel = ctx.file.kernel.short_path,
+            cmdline = ctx.attr.cmdline,
+        ),
+        is_executable = True,
     )
 
-    native.sh_test(
-        name = "ktest",
-        args = [
-            "$(location //osbase/test/ktest)",
-            "$(location :test_initramfs)",
-            "$(location //osbase/test/ktest:linux-testing)",
-            cmdline,
-        ],
-        size = "small",
-        srcs = ["//osbase/test/ktest:test-script"],
-        data = [
-            "//osbase/test/ktest",
-            ":test_initramfs",
-            "//osbase/test/ktest:linux-testing",
-        ],
-    )
+    return [DefaultInfo(
+        executable = script_file,
+        runfiles = ctx.runfiles(
+            files = [ctx.files._ktest[0], initramfs, ctx.file.kernel, ctx.file.tester],
+        ),
+    )]
+
+k_test = rule(
+    implementation = _ktest_impl,
+    doc = """
+        Run a given test program under the Monogon kernel. 
+    """,
+    attrs = {
+        "tester": attr.label(
+            mandatory = True,
+            executable = True,
+            allow_single_file = True,
+            # Runs inside the given kernel, needs to be build for Linux/static
+            cfg = build_static_transition,
+        ),
+        "files": attr.label_keyed_string_dict(
+            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.
+            """,
+            # Attach pure transition to ensure all binaries added to the initramfs are pure/static binaries.
+            cfg = build_pure_transition,
+        ),
+        "files_cc": attr.label_keyed_string_dict(
+            allow_files = True,
+            doc = """
+                 Special case of 'files' for compilation targets that need to be built with the musl toolchain like
+                 go_binary targets which need cgo or cc_binary targets.
+            """,
+            # Attach static transition to all files_cc inputs to ensure they are built with musl and static.
+            cfg = build_static_transition,
+        ),
+        "symlinks": attr.string_dict(
+            default = {},
+            doc = """
+                Symbolic links to create. Similar format as in files and files_cc, so the target of the symlink is the
+                key and the value of it is the location of the symlink itself. Only raw strings are allowed as targets,
+                labels are not permitted. Include the file using files or files_cc, then symlink to its location.
+            """,
+        ),
+        "fsspecs": attr.label_list(
+            default = [],
+            doc = """
+                List of file system specs (osbase.build.fsspec.FSSpec) to also include in the resulting image.
+                These will be merged with all other given attributes.
+            """,
+            providers = [FSSpecInfo],
+            allow_files = True,
+        ),
+        "kernel": attr.label(
+            default = Label("//osbase/test/ktest:linux-testing"),
+            cfg = "exec",
+            allow_single_file = True,
+        ),
+        "cmdline": attr.string(
+            default = "",
+        ),
+        # Tool
+        "_ktest": attr.label(
+            default = Label("//osbase/test/ktest"),
+            cfg = "exec",
+            executable = True,
+            allow_files = True,
+        ),
+        "_ktest_init": attr.label(
+            default = Label("//osbase/test/ktest/init"),
+            cfg = build_pure_transition,
+            executable = True,
+            allow_single_file = True,
+        ),
+        "_mkcpio": attr.label(
+            default = Label("//osbase/build/mkcpio"),
+            executable = True,
+            cfg = "exec",
+        ),
+        "_earlydev": attr.label(
+            default = Label("//osbase/build:earlydev.fsspec"),
+            allow_files = True,
+        ),
+    },
+    test = True,
+)
diff --git a/osbase/test/launch/launch.go b/osbase/test/launch/launch.go
index cf165a1..df84e7d 100644
--- a/osbase/test/launch/launch.go
+++ b/osbase/test/launch/launch.go
@@ -294,7 +294,7 @@
 	}
 
 	var stdErrBuf bytes.Buffer
-	cmd := exec.CommandContext(ctx, "qemu-system-x86_64", append(baseArgs, extraArgs...)...)
+	cmd := exec.CommandContext(ctx, "/usr/bin/qemu-system-x86_64", append(baseArgs, extraArgs...)...)
 	cmd.Stdout = opts.SerialPort
 	cmd.Stderr = &stdErrBuf
 
diff --git a/osbase/verity/BUILD.bazel b/osbase/verity/BUILD.bazel
index 69cd10f..c44dd2a 100644
--- a/osbase/verity/BUILD.bazel
+++ b/osbase/verity/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
-load("//osbase/test/ktest:ktest.bzl", "ktest")
+load("//osbase/test/ktest:ktest.bzl", "k_test")
 
 go_library(
     name = "verity",
@@ -19,7 +19,8 @@
     ],
 )
 
-ktest(
+k_test(
+    name = "ktest",
     cmdline = "ramdisk_size=16384",
     tester = ":verity_test",
 )