osbase/build: move efi.bzl, split and move def.bzl to their corresponding action

This is a small reorganization to make the osbase/build less dependent on each other.

Change-Id: I8c12f04f3bdc98128c5424f142f452c2e094f2e8
Reviewed-on: https://review.monogon.dev/c/monogon/+/3903
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/cloud/agent/takeover/BUILD.bazel b/cloud/agent/takeover/BUILD.bazel
index 61d6480..310a82d 100644
--- a/cloud/agent/takeover/BUILD.bazel
+++ b/cloud/agent/takeover/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_binary")
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 load("//build/static_binary_tarball:def.bzl", "static_binary_tarball")
-load("//osbase/build:def.bzl", "node_initramfs")
+load("//osbase/build/mkcpio:def.bzl", "node_initramfs")
 
 go_library(
     name = "takeover_lib",
diff --git a/metropolis/cli/takeover/BUILD.bazel b/metropolis/cli/takeover/BUILD.bazel
index 08f4f19..fe58518 100644
--- a/metropolis/cli/takeover/BUILD.bazel
+++ b/metropolis/cli/takeover/BUILD.bazel
@@ -1,6 +1,6 @@
 load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_binary")
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-load("//osbase/build:def.bzl", "node_initramfs")
+load("//osbase/build/mkcpio:def.bzl", "node_initramfs")
 
 node_initramfs(
     name = "initramfs",
diff --git a/metropolis/installer/BUILD.bazel b/metropolis/installer/BUILD.bazel
index a2790b4..5792326 100644
--- a/metropolis/installer/BUILD.bazel
+++ b/metropolis/installer/BUILD.bazel
@@ -1,7 +1,7 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-load("//osbase/build:def.bzl", "node_initramfs")
-load("//osbase/build:efi.bzl", "efi_unified_kernel_image")
 load("//osbase/build/genosrelease:defs.bzl", "os_release")
+load("//osbase/build/mkcpio:def.bzl", "node_initramfs")
+load("//osbase/build/mkpayload:def.bzl", "efi_unified_kernel_image")
 
 go_library(
     name = "installer_lib",
diff --git a/metropolis/installer/test/BUILD.bazel b/metropolis/installer/test/BUILD.bazel
index 7390cf6..7f7ad54 100644
--- a/metropolis/installer/test/BUILD.bazel
+++ b/metropolis/installer/test/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_test")
-load("//osbase/build:efi.bzl", "efi_unified_kernel_image")
+load("//osbase/build/mkpayload:def.bzl", "efi_unified_kernel_image")
 
 go_test(
     name = "test_test",
diff --git a/metropolis/installer/test/testos/BUILD.bazel b/metropolis/installer/test/testos/BUILD.bazel
index c7d58f0..c8f1c3f 100644
--- a/metropolis/installer/test/testos/BUILD.bazel
+++ b/metropolis/installer/test/testos/BUILD.bazel
@@ -1,7 +1,8 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
 load("@rules_pkg//:pkg.bzl", "pkg_zip")
-load("//osbase/build:def.bzl", "erofs_image", "verity_image")
-load("//osbase/build:efi.bzl", "efi_unified_kernel_image")
+load("//osbase/build/mkerofs:def.bzl", "erofs_image")
+load("//osbase/build/mkpayload:def.bzl", "efi_unified_kernel_image")
+load("//osbase/build/mkverity:def.bzl", "verity_image")
 
 erofs_image(
     name = "rootfs",
diff --git a/metropolis/node/BUILD.bazel b/metropolis/node/BUILD.bazel
index 109b0f5..12cbbee 100644
--- a/metropolis/node/BUILD.bazel
+++ b/metropolis/node/BUILD.bazel
@@ -1,10 +1,11 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 load("@rules_pkg//:pkg.bzl", "pkg_zip")
 load("//build/go:def.bzl", "go_binary_with_tag")
-load("//osbase/build:def.bzl", "erofs_image", "verity_image")
-load("//osbase/build:efi.bzl", "efi_unified_kernel_image")
 load("//osbase/build/genosrelease:defs.bzl", "os_release")
+load("//osbase/build/mkerofs:def.bzl", "erofs_image")
 load("//osbase/build/mkimage:def.bzl", "node_image")
+load("//osbase/build/mkpayload:def.bzl", "efi_unified_kernel_image")
+load("//osbase/build/mkverity:def.bzl", "verity_image")
 
 go_library(
     name = "node",
diff --git a/metropolis/node/core/update/e2e/testos/testos.bzl b/metropolis/node/core/update/e2e/testos/testos.bzl
index e37ddb5..a38b68d 100644
--- a/metropolis/node/core/update/e2e/testos/testos.bzl
+++ b/metropolis/node/core/update/e2e/testos/testos.bzl
@@ -1,8 +1,9 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary")
 load("@rules_pkg//:mappings.bzl", "pkg_files")
 load("@rules_pkg//:pkg.bzl", "pkg_zip")
-load("//osbase/build:def.bzl", "erofs_image", "verity_image")
-load("//osbase/build:efi.bzl", "efi_unified_kernel_image")
+load("//osbase/build/mkerofs:def.bzl", "erofs_image")
+load("//osbase/build/mkpayload:def.bzl", "efi_unified_kernel_image")
+load("//osbase/build/mkverity:def.bzl", "verity_image")
 
 # Macro for generating multiple TestOS instances to check if the updater works.
 # buildifier: disable=unnamed-macro
diff --git a/metropolis/test/nanoswitch/BUILD.bazel b/metropolis/test/nanoswitch/BUILD.bazel
index 1dbf267..c82f409 100644
--- a/metropolis/test/nanoswitch/BUILD.bazel
+++ b/metropolis/test/nanoswitch/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-load("//osbase/build:def.bzl", "node_initramfs")
+load("//osbase/build/mkcpio:def.bzl", "node_initramfs")
 
 go_library(
     name = "nanoswitch_lib",
diff --git a/osbase/bringup/test/BUILD.bazel b/osbase/bringup/test/BUILD.bazel
index a5f3768..103b2b4 100644
--- a/osbase/bringup/test/BUILD.bazel
+++ b/osbase/bringup/test/BUILD.bazel
@@ -1,6 +1,6 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
-load("//osbase/build:def.bzl", "node_initramfs")
-load("//osbase/build:efi.bzl", "efi_unified_kernel_image")
+load("//osbase/build/mkcpio:def.bzl", "node_initramfs")
+load("//osbase/build/mkpayload:def.bzl", "efi_unified_kernel_image")
 
 go_test(
     name = "test_test",
diff --git a/osbase/build/def.bzl b/osbase/build/def.bzl
index d4b497c..e88614b 100644
--- a/osbase/build/def.bzl
+++ b/osbase/build/def.bzl
@@ -40,277 +40,3 @@
         "//command_line_option:platforms",
     ],
 )
-
-FSSpecInfo = provider(
-    "Provides parts of an FSSpec used to assemble filesystem images",
-    fields = {
-        "spec": "File containing the partial FSSpec as prototext",
-        "referenced": "Files (potentially) referenced by the spec",
-    },
-)
-
-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
-    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.
-    """
-    fs_spec_name = ctx.label.name + ".prototxt"
-    fs_spec = ctx.actions.declare_file(fs_spec_name)
-
-    fs_files = []
-    inputs = []
-    for p, label in ctx.attr.files.items() + ctx.attr.files_cc.items() + extra_files:
-        if not p.startswith("/"):
-            fail("file {} invalid: must begin with /".format(p))
-
-        # 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 = 0o555 if is_executable else 0o444
-        fs_files.append(struct(path = p, source_path = src.path, mode = mode, uid = 0, gid = 0))
-
-    fs_symlinks = []
-    for target, p in ctx.attr.symlinks.items():
-        fs_symlinks.append(struct(path = p, target_path = target))
-
-    fs_spec_content = struct(file = fs_files, directory = [], symbolic_link = fs_symlinks)
-    ctx.actions.write(fs_spec, proto.encode_text(fs_spec_content))
-
-    extra_specs = []
-
-    for fsspec in ctx.attr.fsspecs + extra_fsspecs:
-        if FSSpecInfo in fsspec:
-            fsspec_info = fsspec[FSSpecInfo]
-            extra_specs.append(fsspec_info.spec)
-            for f in fsspec_info.referenced:
-                inputs.append(f)
-        else:
-            # Raw .fsspec prototext. No referenced data allowed.
-            di = fsspec[DefaultInfo]
-            extra_specs += di.files.to_list()
-
-    ctx.actions.run(
-        outputs = [output_file],
-        inputs = [fs_spec] + inputs + extra_specs,
-        tools = [tool],
-        executable = tool,
-        arguments = ["-out", output_file.path, fs_spec.path] + [s.path for s in extra_specs],
-    )
-    return
-
-def _node_initramfs_impl(ctx):
-    initramfs_name = ctx.label.name + ".cpio.zst"
-    initramfs = ctx.actions.declare_file(initramfs_name)
-
-    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]))]
-
-node_initramfs = rule(
-    implementation = _node_initramfs_impl,
-    doc = """
-        Build a node 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.string_keyed_label_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.
-            """,
-            # Attach pure transition to ensure all binaries added to the initramfs are pure/static binaries.
-            cfg = build_pure_transition,
-        ),
-        "files_cc": attr.string_keyed_label_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,
-        ),
-
-        # Tool
-        "_mkcpio": attr.label(
-            default = Label("//osbase/build/mkcpio"),
-            executable = True,
-            cfg = "exec",
-        ),
-    },
-)
-
-def _erofs_image_impl(ctx):
-    fs_name = ctx.label.name + ".img"
-    fs_out = ctx.actions.declare_file(fs_name)
-
-    fsspec_core_impl(ctx, ctx.executable._mkerofs, fs_out)
-
-    return [DefaultInfo(files = depset([fs_out]))]
-
-erofs_image = rule(
-    implementation = _erofs_image_impl,
-    doc = """
-        Build an EROFS. All files specified in files, files_cc and all specified symlinks will be contained.
-        Executable files will have their permissions set to 0555, non-executable files will have
-        their permissions set to 0444. All parent directories will be created with 0555 permissions.
-    """,
-    attrs = {
-        "files": attr.string_keyed_label_dict(
-            mandatory = True,
-            allow_files = True,
-            doc = """
-                Dictionary of Labels to String, placing a given Label's output file in the EROFS 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.string_keyed_label_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,
-        ),
-
-        # Tools, implicit dependencies.
-        "_mkerofs": attr.label(
-            default = Label("//osbase/build/mkerofs"),
-            executable = True,
-            cfg = "exec",
-        ),
-    },
-)
-
-# VerityInfo is emitted by verity_image, and contains a file enclosing a
-# singular dm-verity target table.
-VerityInfo = provider(
-    "Information necessary to mount a single dm-verity target.",
-    fields = {
-        "table": "A file containing the dm-verity target table. See: https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/verity.html",
-    },
-)
-
-def _verity_image_impl(ctx):
-    """
-    Create a new file containing the source image data together with the Verity
-    metadata appended to it, and provide an associated DeviceMapper Verity target
-    table in a separate file, through VerityInfo provider.
-    """
-
-    # Run mkverity.
-    image = ctx.actions.declare_file(ctx.attr.name + ".img")
-    table = ctx.actions.declare_file(ctx.attr.name + ".dmt")
-    ctx.actions.run(
-        mnemonic = "GenVerityImage",
-        progress_message = "Generating a dm-verity image: {}".format(image.short_path),
-        inputs = [ctx.file.source],
-        outputs = [
-            image,
-            table,
-        ],
-        executable = ctx.file._mkverity,
-        arguments = [
-            "-input=" + ctx.file.source.path,
-            "-output=" + image.path,
-            "-table=" + table.path,
-            "-data_alias=" + ctx.attr.rootfs_partlabel,
-            "-hash_alias=" + ctx.attr.rootfs_partlabel,
-        ],
-    )
-
-    return [
-        DefaultInfo(
-            files = depset([image]),
-            runfiles = ctx.runfiles(files = [image]),
-        ),
-        VerityInfo(
-            table = table,
-        ),
-    ]
-
-verity_image = rule(
-    implementation = _verity_image_impl,
-    doc = """
-      Build a dm-verity target image by appending Verity metadata to the source
-      image. A corresponding dm-verity target table will be made available
-      through VerityInfo provider.
-  """,
-    attrs = {
-        "source": attr.label(
-            doc = "A source image.",
-            allow_single_file = True,
-        ),
-        "rootfs_partlabel": attr.string(
-            doc = "GPT partition label of the rootfs to be used with dm-mod.create.",
-            default = "PARTLABEL=METROPOLIS-SYSTEM-X",
-        ),
-        "_mkverity": attr.label(
-            doc = "The mkverity executable needed to generate the image.",
-            default = "//osbase/build/mkverity",
-            allow_single_file = True,
-            executable = True,
-            cfg = "exec",
-        ),
-    },
-)
diff --git a/osbase/build/fsspec/def.bzl b/osbase/build/fsspec/def.bzl
new file mode 100644
index 0000000..8e68069
--- /dev/null
+++ b/osbase/build/fsspec/def.bzl
@@ -0,0 +1,77 @@
+FSSpecInfo = provider(
+    "Provides parts of an FSSpec used to assemble filesystem images",
+    fields = {
+        "spec": "File containing the partial FSSpec as prototext",
+        "referenced": "Files (potentially) referenced by the spec",
+    },
+)
+
+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
+    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.
+    """
+    fs_spec_name = ctx.label.name + ".prototxt"
+    fs_spec = ctx.actions.declare_file(fs_spec_name)
+
+    fs_files = []
+    inputs = []
+    for p, label in ctx.attr.files.items() + ctx.attr.files_cc.items() + extra_files:
+        if not p.startswith("/"):
+            fail("file {} invalid: must begin with /".format(p))
+
+        # 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 = 0o555 if is_executable else 0o444
+        fs_files.append(struct(path = p, source_path = src.path, mode = mode, uid = 0, gid = 0))
+
+    fs_symlinks = []
+    for target, p in ctx.attr.symlinks.items():
+        fs_symlinks.append(struct(path = p, target_path = target))
+
+    fs_spec_content = struct(file = fs_files, directory = [], symbolic_link = fs_symlinks)
+    ctx.actions.write(fs_spec, proto.encode_text(fs_spec_content))
+
+    extra_specs = []
+
+    for fsspec in ctx.attr.fsspecs + extra_fsspecs:
+        if FSSpecInfo in fsspec:
+            fsspec_info = fsspec[FSSpecInfo]
+            extra_specs.append(fsspec_info.spec)
+            for f in fsspec_info.referenced:
+                inputs.append(f)
+        else:
+            # Raw .fsspec prototext. No referenced data allowed.
+            di = fsspec[DefaultInfo]
+            extra_specs += di.files.to_list()
+
+    ctx.actions.run(
+        mnemonic = "GenFSSpecImage",
+        progress_message = "Generating a fsspec based image: {}".format(output_file.short_path),
+        outputs = [output_file],
+        inputs = [fs_spec] + inputs + extra_specs,
+        tools = [tool],
+        executable = tool,
+        arguments = ["-out", output_file.path, fs_spec.path] + [s.path for s in extra_specs],
+    )
+    return
diff --git a/osbase/build/fwprune/def.bzl b/osbase/build/fwprune/def.bzl
index 4633c93..e05d237 100644
--- a/osbase/build/fwprune/def.bzl
+++ b/osbase/build/fwprune/def.bzl
@@ -1,4 +1,4 @@
-load("//osbase/build:def.bzl", "FSSpecInfo")
+load("//osbase/build/fsspec:def.bzl", "FSSpecInfo")
 
 def _fsspec_linux_firmware(ctx):
     fsspec_out = ctx.actions.declare_file(ctx.label.name + ".prototxt")
diff --git a/osbase/build/mkcpio/def.bzl b/osbase/build/mkcpio/def.bzl
new file mode 100644
index 0000000..402314e
--- /dev/null
+++ b/osbase/build/mkcpio/def.bzl
@@ -0,0 +1,65 @@
+load("//osbase/build:def.bzl", "build_pure_transition", "build_static_transition")
+load("//osbase/build/fsspec:def.bzl", "FSSpecInfo", "fsspec_core_impl")
+
+def _node_initramfs_impl(ctx):
+    initramfs_name = ctx.label.name + ".cpio.zst"
+    initramfs = ctx.actions.declare_file(initramfs_name)
+
+    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]))]
+
+node_initramfs = rule(
+    implementation = _node_initramfs_impl,
+    doc = """
+        Build a node 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.string_keyed_label_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.
+            """,
+            # Attach pure transition to ensure all binaries added to the initramfs are pure/static binaries.
+            cfg = build_pure_transition,
+        ),
+        "files_cc": attr.string_keyed_label_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,
+        ),
+
+        # Tool
+        "_mkcpio": attr.label(
+            default = Label("//osbase/build/mkcpio"),
+            executable = True,
+            cfg = "exec",
+        ),
+    },
+)
diff --git a/osbase/build/mkerofs/def.bzl b/osbase/build/mkerofs/def.bzl
new file mode 100644
index 0000000..45f00d8
--- /dev/null
+++ b/osbase/build/mkerofs/def.bzl
@@ -0,0 +1,64 @@
+load("//osbase/build:def.bzl", "build_pure_transition", "build_static_transition")
+load("//osbase/build/fsspec:def.bzl", "FSSpecInfo", "fsspec_core_impl")
+
+def _erofs_image_impl(ctx):
+    fs_name = ctx.label.name + ".img"
+    fs_out = ctx.actions.declare_file(fs_name)
+
+    fsspec_core_impl(ctx, ctx.executable._mkerofs, fs_out)
+
+    return [DefaultInfo(files = depset([fs_out]))]
+
+erofs_image = rule(
+    implementation = _erofs_image_impl,
+    doc = """
+        Build an EROFS. All files specified in files, files_cc and all specified symlinks will be contained.
+        Executable files will have their permissions set to 0555, non-executable files will have
+        their permissions set to 0444. All parent directories will be created with 0555 permissions.
+    """,
+    attrs = {
+        "files": attr.string_keyed_label_dict(
+            mandatory = True,
+            allow_files = True,
+            doc = """
+                Dictionary of Labels to String, placing a given Label's output file in the EROFS 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.string_keyed_label_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,
+        ),
+
+        # Tools, implicit dependencies.
+        "_mkerofs": attr.label(
+            default = Label("//osbase/build/mkerofs"),
+            executable = True,
+            cfg = "exec",
+        ),
+    },
+)
diff --git a/osbase/build/efi.bzl b/osbase/build/mkpayload/def.bzl
similarity index 98%
rename from osbase/build/efi.bzl
rename to osbase/build/mkpayload/def.bzl
index 54b43a6..fe7cf74 100644
--- a/osbase/build/efi.bzl
+++ b/osbase/build/mkpayload/def.bzl
@@ -4,7 +4,7 @@
 """
 
 load("//build/toolchain/llvm-efi:transition.bzl", "build_efi_transition")
-load("//osbase/build:def.bzl", "VerityInfo")
+load("//osbase/build/mkverity:def.bzl", "VerityInfo")
 
 def _efi_unified_kernel_image_impl(ctx):
     # Find the dependency paths to be passed to mkpayload.
diff --git a/osbase/build/mkverity/def.bzl b/osbase/build/mkverity/def.bzl
new file mode 100644
index 0000000..417c883
--- /dev/null
+++ b/osbase/build/mkverity/def.bzl
@@ -0,0 +1,72 @@
+# VerityInfo is emitted by verity_image, and contains a file enclosing a
+# singular dm-verity target table.
+VerityInfo = provider(
+    "Information necessary to mount a single dm-verity target.",
+    fields = {
+        "table": "A file containing the dm-verity target table. See: https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/verity.html",
+    },
+)
+
+def _verity_image_impl(ctx):
+    """
+    Create a new file containing the source image data together with the Verity
+    metadata appended to it, and provide an associated DeviceMapper Verity target
+    table in a separate file, through VerityInfo provider.
+    """
+
+    # Run mkverity.
+    image = ctx.actions.declare_file(ctx.attr.name + ".img")
+    table = ctx.actions.declare_file(ctx.attr.name + ".dmt")
+    ctx.actions.run(
+        mnemonic = "GenVerityImage",
+        progress_message = "Generating a dm-verity image: {}".format(image.short_path),
+        inputs = [ctx.file.source],
+        outputs = [
+            image,
+            table,
+        ],
+        executable = ctx.file._mkverity,
+        arguments = [
+            "-input=" + ctx.file.source.path,
+            "-output=" + image.path,
+            "-table=" + table.path,
+            "-data_alias=" + ctx.attr.rootfs_partlabel,
+            "-hash_alias=" + ctx.attr.rootfs_partlabel,
+        ],
+    )
+
+    return [
+        DefaultInfo(
+            files = depset([image]),
+            runfiles = ctx.runfiles(files = [image]),
+        ),
+        VerityInfo(
+            table = table,
+        ),
+    ]
+
+verity_image = rule(
+    implementation = _verity_image_impl,
+    doc = """
+      Build a dm-verity target image by appending Verity metadata to the source
+      image. A corresponding dm-verity target table will be made available
+      through VerityInfo provider.
+  """,
+    attrs = {
+        "source": attr.label(
+            doc = "A source image.",
+            allow_single_file = True,
+        ),
+        "rootfs_partlabel": attr.string(
+            doc = "GPT partition label of the rootfs to be used with dm-mod.create.",
+            default = "PARTLABEL=METROPOLIS-SYSTEM-X",
+        ),
+        "_mkverity": attr.label(
+            doc = "The mkverity executable needed to generate the image.",
+            default = "//osbase/build/mkverity",
+            allow_single_file = True,
+            executable = True,
+            cfg = "exec",
+        ),
+    },
+)
diff --git a/osbase/test/ktest/ktest.bzl b/osbase/test/ktest/ktest.bzl
index 37efd30..60a90a5 100644
--- a/osbase/test/ktest/ktest.bzl
+++ b/osbase/test/ktest/ktest.bzl
@@ -18,7 +18,8 @@
 Ktest provides a macro to run tests under a normal Metropolis node kernel
 """
 
-load("//osbase/build:def.bzl", "FSSpecInfo", "build_pure_transition", "build_static_transition", "fsspec_core_impl")
+load("//osbase/build:def.bzl", "build_pure_transition", "build_static_transition")
+load("//osbase/build/fsspec:def.bzl", "FSSpecInfo", "fsspec_core_impl")
 
 _KTEST_SCRIPT = """
 #!/usr/bin/env bash