treewide: move build helper to more fitting places
Change-Id: I3d0cfe9283222d403ae369ec9db09201ad511e15
Reviewed-on: https://review.monogon.dev/c/monogon/+/3327
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/osbase/build/BUILD.bazel b/osbase/build/BUILD.bazel
new file mode 100644
index 0000000..8eafa9d
--- /dev/null
+++ b/osbase/build/BUILD.bazel
@@ -0,0 +1 @@
+exports_files(["earlydev.fsspec"])
diff --git a/osbase/build/def.bzl b/osbase/build/def.bzl
new file mode 100644
index 0000000..8e9fcd9
--- /dev/null
+++ b/osbase/build/def.bzl
@@ -0,0 +1,403 @@
+# 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.
+load("@bazel_skylib//lib:paths.bzl", "paths")
+
+def _build_pure_transition_impl(settings, attr):
+ """
+ Transition that enables pure, static build of Go binaries.
+ """
+ race = settings['@io_bazel_rules_go//go/config:race']
+ pure = not race
+
+ return {
+ "@io_bazel_rules_go//go/config:pure": pure,
+ "@io_bazel_rules_go//go/config:static": True,
+ }
+
+build_pure_transition = transition(
+ implementation = _build_pure_transition_impl,
+ inputs = [
+ "@io_bazel_rules_go//go/config:race",
+ ],
+ outputs = [
+ "@io_bazel_rules_go//go/config:pure",
+ "@io_bazel_rules_go//go/config:static",
+ ],
+)
+
+def _build_static_transition_impl(settings, attr):
+ """
+ Transition that enables static builds with CGo and musl for Go binaries.
+ """
+ return {
+ "@io_bazel_rules_go//go/config:static": True,
+ "//command_line_option:platforms": "//build/platforms:linux_amd64_static",
+ }
+
+build_static_transition = transition(
+ implementation = _build_static_transition_impl,
+ inputs = [],
+ outputs = [
+ "@io_bazel_rules_go//go/config:static",
+ "//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):
+ """
+ _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 label, p in ctx.attr.files.items() + ctx.attr.files_cc.items():
+ 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:
+ if FSSpecInfo in fsspec:
+ fsspecInfo = fsspec[FSSpecInfo]
+ extra_specs.append(fsspecInfo.spec)
+ for f in fsspecInfo.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.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.
+ """,
+ # 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,
+ ),
+
+ # 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.label_keyed_string_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.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,
+ ),
+
+ # Tools, implicit dependencies.
+ "_mkerofs": attr.label(
+ default = Label("//osbase/build/mkerofs"),
+ executable = True,
+ cfg = "host",
+ ),
+ },
+)
+
+# VerityConfig is emitted by verity_image, and contains a file enclosing a
+# singular dm-verity target table.
+VerityConfig = provider(
+ "Configuration 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 VerityConfig 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",
+ 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]),
+ ),
+ VerityConfig(
+ 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 VerityConfig 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 = "host",
+ ),
+ },
+)
+
+# From Aspect's bazel-lib under Apache 2.0
+def _transition_platform_impl(_, attr):
+ return {"//command_line_option:platforms": str(attr.target_platform)}
+
+# Transition from any input configuration to one that includes the
+# --platforms command-line flag.
+_transition_platform = transition(
+ implementation = _transition_platform_impl,
+ inputs = [],
+ outputs = ["//command_line_option:platforms"],
+)
+
+
+def _platform_transition_binary_impl(ctx):
+ # We need to forward the DefaultInfo provider from the underlying rule.
+ # Unfortunately, we can't do this directly, because Bazel requires that the executable to run
+ # is actually generated by this rule, so we need to symlink to it, and generate a synthetic
+ # forwarding DefaultInfo.
+
+ result = []
+ binary = ctx.attr.binary[0]
+
+ default_info = binary[DefaultInfo]
+ files = default_info.files
+ new_executable = None
+ original_executable = default_info.files_to_run.executable
+ runfiles = default_info.default_runfiles
+
+ if not original_executable:
+ fail("Cannot transition a 'binary' that is not executable")
+
+ new_executable_name = ctx.attr.basename if ctx.attr.basename else original_executable.basename
+
+ # In order for the symlink to have the same basename as the original
+ # executable (important in the case of proto plugins), put it in a
+ # subdirectory named after the label to prevent collisions.
+ new_executable = ctx.actions.declare_file(paths.join(ctx.label.name, new_executable_name))
+ ctx.actions.symlink(
+ output = new_executable,
+ target_file = original_executable,
+ is_executable = True,
+ )
+ files = depset(direct = [new_executable], transitive = [files])
+ runfiles = runfiles.merge(ctx.runfiles([new_executable]))
+
+ result.append(
+ DefaultInfo(
+ files = files,
+ runfiles = runfiles,
+ executable = new_executable,
+ ),
+ )
+
+ return result
+
+platform_transition_binary = rule(
+ implementation = _platform_transition_binary_impl,
+ attrs = {
+ "basename": attr.string(),
+ "binary": attr.label(allow_files = True, cfg = _transition_platform),
+ "target_platform": attr.label(
+ doc = "The target platform to transition the binary.",
+ mandatory = True,
+ ),
+ "_allowlist_function_transition": attr.label(
+ default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+ ),
+ },
+ executable = True,
+ doc = "Transitions the binary to use the provided platform.",
+)
\ No newline at end of file
diff --git a/osbase/build/earlydev.fsspec b/osbase/build/earlydev.fsspec
new file mode 100644
index 0000000..a7d2ea4
--- /dev/null
+++ b/osbase/build/earlydev.fsspec
@@ -0,0 +1,54 @@
+# Critical /dev files which should be present as early as possible, ie. be baked
+# into filesystem images.
+
+# At least /dev/console and /dev/null are required to exist for Linux
+# to properly boot an init. Here we additionally include important device nodes
+# like /dev/kmsg and /dev/ptmx which might need to be available before a proper
+# device manager (ie. devtmpfs) is launched.
+special_file <
+ path: "/dev/console"
+ type: CHARACTER_DEV
+ major: 5 minor: 1
+ mode: 0600 uid: 0 gid: 0
+>
+special_file <
+ path: "/dev/ptmx"
+ type: CHARACTER_DEV
+ major: 5 minor: 2
+ mode: 0644 uid: 0 gid: 0
+>
+special_file <
+ path: "/dev/null"
+ type: CHARACTER_DEV
+ major: 1 minor: 3
+ mode: 0644 uid: 0 gid: 0
+>
+special_file <
+ path: "/dev/kmsg"
+ type: CHARACTER_DEV
+ major: 1 minor: 11
+ mode: 0644 uid: 0 gid: 0
+>
+
+
+# Metropolis core logs to /dev/ttyS{0,1} and /dev/tty0 by default, we want
+# these to also be present before devtmpfs is mounted so that minit can
+# log there, too.
+special_file <
+ path: "/dev/tty0"
+ type: CHARACTER_DEV
+ major: 4 minor: 0
+ mode: 0600 uid: 0 gid: 0
+>
+special_file <
+ path: "/dev/ttyS0"
+ type: CHARACTER_DEV
+ major: 4 minor: 64
+ mode: 0660 uid: 0 gid: 0
+>
+special_file <
+ path: "/dev/ttyS1"
+ type: CHARACTER_DEV
+ major: 4 minor: 65
+ mode: 0660 uid: 0 gid: 0
+>
\ No newline at end of file
diff --git a/osbase/build/efi.bzl b/osbase/build/efi.bzl
new file mode 100644
index 0000000..2f5f363
--- /dev/null
+++ b/osbase/build/efi.bzl
@@ -0,0 +1,135 @@
+"""Rules for generating EFI unified kernel images. These are EFI-bootable PE/COFF files containing a stub loader,
+a kernel, and optional commandline and initramfs in one file.
+See https://systemd.io/BOOT_LOADER_SPECIFICATION/#type-2-efi-unified-kernel-images for more information.
+"""
+
+load("//build/toolchain/llvm-efi:transition.bzl", "build_efi_transition")
+load("//osbase/build:def.bzl", "VerityConfig")
+
+def _efi_unified_kernel_image_impl(ctx):
+ # Find the dependency paths to be passed to mkpayload.
+ deps = {
+ "linux": ctx.file.kernel,
+ "osrel": ctx.file.os_release,
+ "splash": ctx.file.splash,
+ "stub": ctx.file.stub,
+ }
+
+ # Since cmdline is a string attribute, put it into a file, then append
+ # that file to deps.
+ if ctx.attr.cmdline and ctx.attr.cmdline != "":
+ cmdline = ctx.actions.declare_file("cmdline")
+ ctx.actions.write(
+ output = cmdline,
+ content = ctx.attr.cmdline,
+ )
+ deps["cmdline"] = cmdline
+
+ # Get the dm-verity target table from VerityConfig provider.
+ if ctx.attr.verity:
+ deps["rootfs_dm_table"] = ctx.attr.verity[VerityConfig].table
+
+ # Format deps into command line arguments while keeping track of mkpayload
+ # runtime inputs.
+ args = []
+ inputs = []
+ for name, file in deps.items():
+ if file:
+ args.append("-{}={}".format(name, file.path))
+ inputs.append(file)
+
+ for file in ctx.files.initrd:
+ args.append("-initrd={}".format(file.path))
+ inputs.append(file)
+
+ # Append the output parameter separately, as it doesn't belong with the
+ # runtime inputs.
+ image = ctx.actions.declare_file(ctx.attr.name + ".efi")
+ args.append("-output={}".format(image.path))
+
+ # Append the objcopy parameter separately, as it's not of File type, and
+ # it does not constitute an input, since it's part of the toolchain.
+ objcopy = ctx.toolchains["@bazel_tools//tools/cpp:toolchain_type"].cc.objcopy_executable
+ args.append("-objcopy={}".format(objcopy))
+
+ # Run mkpayload.
+ ctx.actions.run(
+ mnemonic = "GenEFIKernelImage",
+ progress_message = "Generating EFI unified kernel image",
+ inputs = inputs,
+ outputs = [image],
+ executable = ctx.file._mkpayload,
+ arguments = args,
+ )
+
+ # Return the unified kernel image file.
+ return [DefaultInfo(files = depset([image]), runfiles = ctx.runfiles(files = [image]))]
+
+efi_unified_kernel_image = rule(
+ implementation = _efi_unified_kernel_image_impl,
+ attrs = {
+ "kernel": attr.label(
+ doc = "The Linux kernel executable bzImage. Needs to have EFI handover and EFI stub enabled.",
+ mandatory = True,
+ allow_single_file = True,
+ ),
+ "cmdline": attr.string(
+ doc = "The kernel commandline to be embedded.",
+ ),
+ "initrd": attr.label_list(
+ doc = """
+ List of payloads to concatenate and supply as the initrd parameter to Linux when it boots.
+ The name stems from the time Linux booted from an initial ram disk (initrd), but it's now
+ a catch-all for a bunch of different larger payload for early Linux initialization.
+
+ In Linux 5.15 this can first contain an arbitrary amount of uncompressed cpio archives
+ with directories being optional which is accessed by earlycpio. This is used for both
+ early microcode loading and ACPI table overrides. This can then be followed by an arbitrary
+ amount of compressed cpio archives (even with different compression methods) which will
+ together make up the initramfs. The initramfs is only booted into if it contains either
+ /init or whatever file is specified as init= in cmdline. Technically depending on kernel
+ flags you might be able to supply an actual initrd, i.e. an image of a disk loaded into
+ RAM, but that has been deprecated for nearly 2 decades and should really not be used.
+
+ For kernels designed to run on physical machines this should at least contain microcode,
+ optionally followed by a compressed initramfs. For kernels only used in virtualized
+ setups the microcode can be left out and if no initramfs is needed this option can
+ be omitted completely.
+ """,
+ allow_files = True,
+ ),
+ "os_release": attr.label(
+ doc = """
+ The os-release file identifying the operating system.
+ See https://www.freedesktop.org/software/systemd/man/os-release.html for format.
+ """,
+ allow_single_file = True,
+ ),
+ "splash": attr.label(
+ doc = "An image in BMP format which will be displayed as a splash screen until the kernel takes over.",
+ allow_single_file = True,
+ ),
+ "stub": attr.label(
+ doc = "The stub executable itself as a PE/COFF executable.",
+ default = "@efistub//:efistub",
+ allow_single_file = True,
+ executable = True,
+ cfg = build_efi_transition,
+ ),
+ "verity": attr.label(
+ doc = "The DeviceMapper Verity rootfs target table.",
+ allow_single_file = True,
+ providers = [DefaultInfo, VerityConfig],
+ ),
+ "_mkpayload": attr.label(
+ doc = "The mkpayload executable.",
+ default = "//osbase/build/mkpayload",
+ allow_single_file = True,
+ executable = True,
+ cfg = "exec",
+ ),
+ },
+ toolchains = [
+ "@bazel_tools//tools/cpp:toolchain_type"
+ ],
+)
diff --git a/osbase/build/fsspec/BUILD.bazel b/osbase/build/fsspec/BUILD.bazel
new file mode 100644
index 0000000..b719522
--- /dev/null
+++ b/osbase/build/fsspec/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+ name = "spec_proto",
+ srcs = ["spec.proto"],
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "fsspec",
+ srcs = ["utils.go"],
+ embed = [":fsspec_go_proto"],
+ importpath = "source.monogon.dev/osbase/build/fsspec",
+ visibility = ["//visibility:public"],
+ deps = ["@org_golang_google_protobuf//encoding/prototext"],
+)
+
+go_proto_library(
+ name = "fsspec_go_proto",
+ importpath = "source.monogon.dev/osbase/build/fsspec",
+ proto = ":spec_proto",
+ visibility = ["//visibility:public"],
+)
diff --git a/osbase/build/fsspec/spec.proto b/osbase/build/fsspec/spec.proto
new file mode 100644
index 0000000..487eebf
--- /dev/null
+++ b/osbase/build/fsspec/spec.proto
@@ -0,0 +1,98 @@
+// 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.
+
+syntax = "proto3";
+
+package osbase.node.build.fsspec;
+option go_package = "source.monogon.dev/osbase/build/fsspec";
+
+// FSSpec is the spec from which a filesystem is generated. It consists of files, directories and symbolic
+// links. Directories are also automatically inferred when required for the placement of files or symbolic
+// links. Inferred directories always have uid 0, gid 0 and permissions 0555. This can be overridden by
+// explicitly specifying a directory at a given path.
+message FSSpec {
+ repeated File file = 1;
+ repeated Directory directory = 2;
+ repeated SymbolicLink symbolic_link = 3;
+ repeated SpecialFile special_file = 4;
+}
+
+// For internal use only. Represents all supported inodes in a oneof.
+message Inode {
+ oneof type {
+ File file = 1;
+ Directory directory = 2;
+ SymbolicLink symbolic_link = 3;
+ SpecialFile special_file = 4;
+ }
+}
+
+message File {
+ // The path where the file ends up in the filesystem.
+ string path = 1;
+ // The path on the host filesystem where the file contents should be taken from.
+ string source_path = 2;
+ // Unix permission bits
+ uint32 mode = 3;
+ // Owner uid
+ uint32 uid = 4;
+ // Owner gid
+ uint32 gid = 5;
+}
+
+message Directory {
+ // The path where the directory ends up in the filesystem.
+ string path = 1;
+ // Unix permission bits
+ uint32 mode = 2;
+ // Owner uid
+ uint32 uid = 3;
+ // Owner gid
+ uint32 gid = 4;
+}
+
+message SymbolicLink {
+ // The path where the symbolic link ends up in the filesystem.
+ string path = 1;
+ // The path to which the symbolic link resolves to.
+ string target_path = 2;
+}
+
+message SpecialFile {
+ // The path where the special file ends up in the filesystem.
+ string path = 1;
+
+ enum Type {
+ CHARACTER_DEV = 0;
+ BLOCK_DEV = 1;
+ FIFO = 2;
+ }
+
+ // Type of special file.
+ Type type = 2;
+
+ // The major device number of the special file.
+ uint32 major = 3;
+ // The minor number of the special file. Ignored for FIFO-type special files.
+ uint32 minor = 4;
+
+ // Unix permission bits
+ uint32 mode = 5;
+ // Owner uid
+ uint32 uid = 6;
+ // Owner gid
+ uint32 gid = 7;
+}
\ No newline at end of file
diff --git a/osbase/build/fsspec/utils.go b/osbase/build/fsspec/utils.go
new file mode 100644
index 0000000..f5a45e3
--- /dev/null
+++ b/osbase/build/fsspec/utils.go
@@ -0,0 +1,30 @@
+package fsspec
+
+import (
+ "fmt"
+ "os"
+
+ "google.golang.org/protobuf/encoding/prototext"
+)
+
+// ReadMergeSpecs reads FSSpecs from all files in paths and merges them into
+// a single FSSpec.
+func ReadMergeSpecs(paths []string) (*FSSpec, error) {
+ var mergedSpec FSSpec
+ for _, p := range paths {
+ specRaw, err := os.ReadFile(p)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open spec: %w", err)
+ }
+
+ var spec FSSpec
+ if err := prototext.Unmarshal(specRaw, &spec); err != nil {
+ return nil, fmt.Errorf("failed to parse spec %q: %w", p, err)
+ }
+ mergedSpec.File = append(mergedSpec.File, spec.File...)
+ mergedSpec.Directory = append(mergedSpec.Directory, spec.Directory...)
+ mergedSpec.SymbolicLink = append(mergedSpec.SymbolicLink, spec.SymbolicLink...)
+ mergedSpec.SpecialFile = append(mergedSpec.SpecialFile, spec.SpecialFile...)
+ }
+ return &mergedSpec, nil
+}
diff --git a/osbase/build/fwprune/BUILD.bazel b/osbase/build/fwprune/BUILD.bazel
new file mode 100644
index 0000000..d9b4718
--- /dev/null
+++ b/osbase/build/fwprune/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "fwprune_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/osbase/build/fwprune",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//osbase/build/fsspec",
+ "//osbase/kmod",
+ "@org_golang_google_protobuf//encoding/prototext",
+ "@org_golang_google_protobuf//proto",
+ ],
+)
+
+go_binary(
+ name = "fwprune",
+ embed = [":fwprune_lib"],
+ visibility = ["//visibility:public"],
+)
diff --git a/osbase/build/fwprune/def.bzl b/osbase/build/fwprune/def.bzl
new file mode 100644
index 0000000..4633c93
--- /dev/null
+++ b/osbase/build/fwprune/def.bzl
@@ -0,0 +1,76 @@
+load("//osbase/build:def.bzl", "FSSpecInfo")
+
+def _fsspec_linux_firmware(ctx):
+ fsspec_out = ctx.actions.declare_file(ctx.label.name + ".prototxt")
+
+ fwlist = ctx.actions.declare_file(ctx.label.name + "-fwlist.txt")
+ ctx.actions.write(
+ output = fwlist,
+ content = "\n".join([f.path for f in ctx.files.firmware_files]),
+ )
+
+ modinfo = ctx.attr.kernel[OutputGroupInfo].modinfo.to_list()[0]
+ modules = ctx.attr.kernel[OutputGroupInfo].modules.to_list()[0]
+
+ meta_out = ctx.actions.declare_file(ctx.label.name + "-meta.pb")
+
+ ctx.actions.run(
+ outputs = [fsspec_out, meta_out],
+ inputs = [fwlist, modinfo, modules, ctx.file.metadata] + ctx.files.firmware_files,
+ tools = [ctx.executable._fwprune],
+ executable = ctx.executable._fwprune,
+ arguments = [
+ "-modinfo",
+ modinfo.path,
+ "-modules",
+ modules.path,
+ "-firmware-file-list",
+ fwlist.path,
+ "-firmware-whence",
+ ctx.file.metadata.path,
+ "-out-meta",
+ meta_out.path,
+ "-out-fsspec",
+ fsspec_out.path,
+ ],
+ )
+
+ return [DefaultInfo(files = depset([fsspec_out])), FSSpecInfo(spec = fsspec_out, referenced = ctx.files.firmware_files + [modules, meta_out])]
+
+fsspec_linux_firmware = rule(
+ implementation = _fsspec_linux_firmware,
+ doc = """
+ Generates a partial filesystem spec containing all firmware files required by a given kernel at the
+ default firmware load path (/lib/firmware).
+ """,
+ attrs = {
+ "firmware_files": attr.label_list(
+ mandatory = True,
+ allow_files = True,
+ doc = """
+ List of firmware files. Generally at least a filegroup of the linux-firmware repository should
+ be in here.
+ """,
+ ),
+ "metadata": attr.label(
+ mandatory = True,
+ allow_single_file = True,
+ doc = """
+ The metadata file for the Linux firmware. Currently this is the WHENCE file at the root of the
+ linux-firmware repository. Used for resolving additional links.
+ """,
+ ),
+ "kernel": attr.label(
+ doc = """
+ Kernel for which firmware should be selected. Needs to have a modinfo OutputGroup.
+ """,
+ ),
+
+ # Tool
+ "_fwprune": attr.label(
+ default = Label("//osbase/build/fwprune"),
+ executable = True,
+ cfg = "exec",
+ ),
+ },
+)
diff --git a/osbase/build/fwprune/main.go b/osbase/build/fwprune/main.go
new file mode 100644
index 0000000..0bedcfd
--- /dev/null
+++ b/osbase/build/fwprune/main.go
@@ -0,0 +1,218 @@
+// fwprune is a buildsystem utility that filters linux-firmware repository
+// contents to include only files required by the built-in kernel modules,
+// that are specified in modules.builtin.modinfo.
+// (see: https://www.kernel.org/doc/Documentation/kbuild/kbuild.txt)
+package main
+
+import (
+ "debug/elf"
+ "flag"
+ "io/fs"
+ "log"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+
+ "google.golang.org/protobuf/encoding/prototext"
+ "google.golang.org/protobuf/proto"
+
+ "source.monogon.dev/osbase/build/fsspec"
+ "source.monogon.dev/osbase/kmod"
+)
+
+// linkRegexp parses the Link: lines in the WHENCE file. This does not have
+// an official grammar, the regexp has been written in an approximation of
+// the original parsing algorithm at @linux-firmware//:copy_firmware.sh.
+var linkRegexp = regexp.MustCompile(`(?m:^Link:\s*([^\s]+)\s+->\s+([^\s]+)\s*$)`)
+
+var (
+ modinfoPath = flag.String("modinfo", "", "Path to the modules.builtin.modinfo file built with the kernel")
+ modulesPath = flag.String("modules", "", "Path to the directory containing the dynamically loaded kernel modules (.ko files)")
+ firmwareListPath = flag.String("firmware-file-list", "", "Path to a file containing a newline-separated list of paths to firmware files")
+ whenceFilePath = flag.String("firmware-whence", "", "Path to the linux-firmware WHENCE file containing aliases for firmware files")
+ outMetaPath = flag.String("out-meta", "", "Path where the resulting module metadata protobuf file should be created")
+ outFSSpecPath = flag.String("out-fsspec", "", "Path where the resulting fsspec should be created")
+)
+
+func main() {
+ flag.Parse()
+ if *modinfoPath == "" || *modulesPath == "" || *firmwareListPath == "" ||
+ *whenceFilePath == "" || *outMetaPath == "" || *outFSSpecPath == "" {
+ log.Fatal("all flags are required and need to be provided")
+ }
+
+ allFirmwareData, err := os.ReadFile(*firmwareListPath)
+ if err != nil {
+ log.Fatalf("Failed to read firmware source list: %v", err)
+ }
+ allFirmwarePaths := strings.Split(string(allFirmwareData), "\n")
+
+ // Create a look-up table of all possible suffixes to their full paths as
+ // this is much faster at O(n) than calling strings.HasSuffix for every
+ // possible combination which is O(n^2).
+ // For example a build output at out/a/b/c.bin will be entered into
+ // the suffix LUT as build as out/a/b/c.bin, a/b/c.bin, b/c.bin and c.bin.
+ // If the firmware then requests b/c.bin, the output path is contained in
+ // the suffix LUT.
+ suffixLUT := make(map[string]string)
+ for _, firmwarePath := range allFirmwarePaths {
+ pathParts := strings.Split(firmwarePath, string(os.PathSeparator))
+ for i := range pathParts {
+ suffixLUT[path.Join(pathParts[i:]...)] = firmwarePath
+ }
+ }
+
+ // The linux-firmware repo contains a WHENCE file which contains (among
+ // other information) aliases for firmware which should be symlinked.
+ // Open this file and create a map of aliases in it.
+ linkMap := make(map[string]string)
+ metadata, err := os.ReadFile(*whenceFilePath)
+ if err != nil {
+ log.Fatalf("Failed to read metadata file: %v", err)
+ }
+ linksRaw := linkRegexp.FindAllStringSubmatch(string(metadata), -1)
+ for _, link := range linksRaw {
+ // For links we know the exact path referenced by kernel drives so
+ // a suffix LUT is unnecessary.
+ linkMap[link[1]] = link[2]
+ }
+
+ // Collect module metadata (modinfo) from both built-in modules via the
+ // kbuild-generated metadata file as well as from the loadable modules by
+ // walking them.
+ var files []*fsspec.File
+ var symlinks []*fsspec.SymbolicLink
+
+ mi, err := os.Open(*modinfoPath)
+ if err != nil {
+ log.Fatalf("While reading modinfo: %v", err)
+ }
+ modMeta, err := kmod.GetBuiltinModulesInfo(mi)
+ if err != nil {
+ log.Fatalf("Failed to read modules modinfo data: %v", err)
+ }
+
+ err = filepath.WalkDir(*modulesPath, func(p string, d fs.DirEntry, err error) error {
+ if err != nil {
+ log.Fatal(err)
+ }
+ if d.IsDir() {
+ return nil
+ }
+ mod, err := elf.Open(p)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer mod.Close()
+ out, err := kmod.GetModuleInfo(mod)
+ if err != nil {
+ log.Fatal(err)
+ }
+ relPath, err := filepath.Rel(*modulesPath, p)
+ if err != nil {
+ return err
+ }
+ // Add path information for MakeMetaFromModuleInfo.
+ out["path"] = []string{relPath}
+ modMeta = append(modMeta, out)
+ files = append(files, &fsspec.File{
+ Path: path.Join("/lib/modules", relPath),
+ SourcePath: filepath.Join(*modulesPath, relPath),
+ Mode: 0555,
+ })
+ return nil
+ })
+ if err != nil {
+ log.Fatalf("Error walking modules: %v", err)
+ }
+
+ // Generate loading metadata from all known modules.
+ meta, err := kmod.MakeMetaFromModuleInfo(modMeta)
+ if err != nil {
+ log.Fatal(err)
+ }
+ metaRaw, err := proto.Marshal(meta)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := os.WriteFile(*outMetaPath, metaRaw, 0640); err != nil {
+ log.Fatal(err)
+ }
+ files = append(files, &fsspec.File{
+ Path: "/lib/modules/meta.pb",
+ SourcePath: *outMetaPath,
+ Mode: 0444,
+ })
+
+ // Create set of all firmware paths required by modules
+ fwset := make(map[string]bool)
+ for _, m := range modMeta {
+ if len(m["path"]) == 0 && len(m.Firmware()) > 0 {
+ log.Fatalf("Module %v is built-in, but requires firmware. Linux does not support this in all configurations.", m.Name())
+ }
+ for _, fw := range m.Firmware() {
+ fwset[fw] = true
+ }
+ }
+
+ // Convert set to list and sort for determinism
+ fwp := make([]string, 0, len(fwset))
+ for p := range fwset {
+ fwp = append(fwp, p)
+ }
+ sort.Strings(fwp)
+
+ // This function is called for every requested firmware file and adds and
+ // resolves symlinks until it finds the target file and adds that too.
+ populatedPaths := make(map[string]bool)
+ var chaseReference func(string)
+ chaseReference = func(p string) {
+ if populatedPaths[p] {
+ // Bail if path is already populated. Because of the DAG-like
+ // property of links in filesystems everything transitively pointed
+ // to by anything at this path has already been included.
+ return
+ }
+ placedPath := path.Join("/lib/firmware", p)
+ if linkTarget := linkMap[p]; linkTarget != "" {
+ symlinks = append(symlinks, &fsspec.SymbolicLink{
+ Path: placedPath,
+ TargetPath: linkTarget,
+ })
+ populatedPaths[p] = true
+ // Symlinks are relative to their place, resolve them to be relative
+ // to the firmware root directory.
+ chaseReference(path.Join(path.Dir(p), linkTarget))
+ return
+ }
+ sourcePath := suffixLUT[p]
+ if sourcePath == "" {
+ // This should not be fatal as sometimes linux-firmware cannot
+ // ship all firmware usable by the kernel for mostly legal reasons.
+ log.Printf("WARNING: Requested firmware %q not found", p)
+ return
+ }
+ files = append(files, &fsspec.File{
+ Path: path.Join("/lib/firmware", p),
+ Mode: 0444,
+ SourcePath: sourcePath,
+ })
+ populatedPaths[p] = true
+ }
+
+ for _, p := range fwp {
+ chaseReference(p)
+ }
+ // Format output in a both human- and machine-readable form
+ marshalOpts := prototext.MarshalOptions{Multiline: true, Indent: " "}
+ fsspecRaw, err := marshalOpts.Marshal(&fsspec.FSSpec{File: files, SymbolicLink: symlinks})
+ if err != nil {
+ log.Fatalf("failed to marshal fsspec: %v", err)
+ }
+ if err := os.WriteFile(*outFSSpecPath, fsspecRaw, 0644); err != nil {
+ log.Fatalf("failed writing output: %v", err)
+ }
+}
diff --git a/osbase/build/genosrelease/BUILD.bazel b/osbase/build/genosrelease/BUILD.bazel
new file mode 100644
index 0000000..f845cae
--- /dev/null
+++ b/osbase/build/genosrelease/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "genosrelease_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/osbase/build/genosrelease",
+ visibility = ["//visibility:private"],
+ deps = ["@com_github_joho_godotenv//:godotenv"],
+)
+
+go_binary(
+ name = "genosrelease",
+ embed = [":genosrelease_lib"],
+ visibility = [
+ "//metropolis/installer:__subpackages__",
+ "//metropolis/node:__subpackages__",
+ ],
+)
diff --git a/osbase/build/genosrelease/defs.bzl b/osbase/build/genosrelease/defs.bzl
new file mode 100644
index 0000000..6fed483
--- /dev/null
+++ b/osbase/build/genosrelease/defs.bzl
@@ -0,0 +1,54 @@
+# 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 _os_release_impl(ctx):
+ ctx.actions.run(
+ mnemonic = "GenOSRelease",
+ progress_message = "Generating os-release",
+ inputs = [ctx.info_file],
+ outputs = [ctx.outputs.out],
+ executable = ctx.executable._genosrelease,
+ arguments = [
+ "-status_file",
+ ctx.info_file.path,
+ "-out_file",
+ ctx.outputs.out.path,
+ "-stamp_var",
+ ctx.attr.stamp_var,
+ "-name",
+ ctx.attr.os_name,
+ "-id",
+ ctx.attr.os_id,
+ ],
+ )
+
+os_release = rule(
+ implementation = _os_release_impl,
+ attrs = {
+ "os_name": attr.string(mandatory = True),
+ "os_id": attr.string(mandatory = True),
+ "stamp_var": attr.string(mandatory = True),
+ "_genosrelease": attr.label(
+ default = Label("//osbase/build/genosrelease"),
+ cfg = "host",
+ executable = True,
+ allow_files = True,
+ ),
+ },
+ outputs = {
+ "out": "os-release",
+ },
+)
diff --git a/osbase/build/genosrelease/main.go b/osbase/build/genosrelease/main.go
new file mode 100644
index 0000000..adb8202
--- /dev/null
+++ b/osbase/build/genosrelease/main.go
@@ -0,0 +1,79 @@
+// 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.
+
+// genosrelease provides rudimentary support to generate os-release files
+// following the freedesktop spec from arguments and stamping
+//
+// https://www.freedesktop.org/software/systemd/man/os-release.html
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/joho/godotenv"
+)
+
+var (
+ flagStatusFile = flag.String("status_file", "", "path to bazel workspace status file")
+ flagOutFile = flag.String("out_file", "os-release", "path to os-release output file")
+ flagStampVar = flag.String("stamp_var", "", "variable to use as version from the workspace status file")
+ flagName = flag.String("name", "", "name parameter (see freedesktop spec)")
+ flagID = flag.String("id", "", "id parameter (see freedesktop spec)")
+)
+
+func main() {
+ flag.Parse()
+ statusFileContent, err := os.ReadFile(*flagStatusFile)
+ if err != nil {
+ fmt.Printf("Failed to open bazel workspace status file: %v\n", err)
+ os.Exit(1)
+ }
+ statusVars := make(map[string]string)
+ for _, line := range strings.Split(string(statusFileContent), "\n") {
+ line = strings.TrimSpace(line)
+ parts := strings.Fields(line)
+ if len(parts) != 2 {
+ continue
+ }
+ statusVars[parts[0]] = parts[1]
+ }
+
+ version, ok := statusVars[*flagStampVar]
+ if !ok {
+ fmt.Printf("%v key not set in bazel workspace status file\n", *flagStampVar)
+ os.Exit(1)
+ }
+ // As specified by https://www.freedesktop.org/software/systemd/man/os-release.html
+ osReleaseVars := map[string]string{
+ "NAME": *flagName,
+ "ID": *flagID,
+ "VERSION": version,
+ "VERSION_ID": version,
+ "PRETTY_NAME": *flagName + " " + version,
+ }
+ osReleaseContent, err := godotenv.Marshal(osReleaseVars)
+ if err != nil {
+ fmt.Printf("Failed to encode os-release file: %v\n", err)
+ os.Exit(1)
+ }
+ if err := os.WriteFile(*flagOutFile, []byte(osReleaseContent), 0644); err != nil {
+ fmt.Printf("Failed to write os-release file: %v\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/osbase/build/kconfig-patcher/BUILD.bazel b/osbase/build/kconfig-patcher/BUILD.bazel
new file mode 100644
index 0000000..59ee391
--- /dev/null
+++ b/osbase/build/kconfig-patcher/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
+
+go_library(
+ name = "kconfig-patcher_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/osbase/build/kconfig-patcher",
+ visibility = ["//visibility:private"],
+)
+
+go_binary(
+ name = "kconfig-patcher",
+ embed = [":kconfig-patcher_lib"],
+ visibility = [
+ "//metropolis/node:__pkg__",
+ "//osbase/test/ktest:__pkg__",
+ ],
+)
+
+go_test(
+ name = "kconfig-patcher_test",
+ srcs = ["main_test.go"],
+ embed = [":kconfig-patcher_lib"],
+)
diff --git a/osbase/build/kconfig-patcher/kconfig-patcher.bzl b/osbase/build/kconfig-patcher/kconfig-patcher.bzl
new file mode 100644
index 0000000..39e786e
--- /dev/null
+++ b/osbase/build/kconfig-patcher/kconfig-patcher.bzl
@@ -0,0 +1,33 @@
+# 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.
+
+"""Override configs in a Linux kernel Kconfig
+"""
+
+def kconfig_patch(name, src, out, override_configs, **kwargs):
+ native.genrule(
+ name = name,
+ srcs = [src],
+ outs = [out],
+ tools = [
+ "//osbase/build/kconfig-patcher",
+ ],
+ cmd = """
+ $(location //osbase/build/kconfig-patcher) \
+ -in $< -out $@ '%s'
+ """ % struct(overrides = override_configs).to_json(),
+ **kwargs
+ )
diff --git a/osbase/build/kconfig-patcher/main.go b/osbase/build/kconfig-patcher/main.go
new file mode 100644
index 0000000..1b5f24f
--- /dev/null
+++ b/osbase/build/kconfig-patcher/main.go
@@ -0,0 +1,98 @@
+// 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 (
+ "bufio"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+)
+
+var (
+ inPath = flag.String("in", "", "Path to input Kconfig")
+ outPath = flag.String("out", "", "Path to output Kconfig")
+)
+
+func main() {
+ flag.Parse()
+ if *inPath == "" || *outPath == "" {
+ flag.PrintDefaults()
+ os.Exit(2)
+ }
+ inFile, err := os.Open(*inPath)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to open input Kconfig: %v\n", err)
+ os.Exit(1)
+ }
+ outFile, err := os.Create(*outPath)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to create output Kconfig: %v\n", err)
+ os.Exit(1)
+ }
+ var config struct {
+ Overrides map[string]string `json:"overrides"`
+ }
+ if err := json.Unmarshal([]byte(flag.Arg(0)), &config); err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to parse overrides: %v\n", err)
+ os.Exit(1)
+ }
+ err = patchKconfig(inFile, outFile, config.Overrides)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to patch: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func patchKconfig(inFile io.Reader, outFile io.Writer, overrides map[string]string) error {
+ scanner := bufio.NewScanner(inFile)
+ for scanner.Scan() {
+ if scanner.Err() != nil {
+ return scanner.Err()
+ }
+ line := scanner.Text()
+ cleanLine := strings.TrimSpace(line)
+ if strings.HasPrefix(cleanLine, "#") || cleanLine == "" {
+ // Pass through comments and empty lines
+ fmt.Fprintln(outFile, line)
+ } else {
+ // Line contains a configuration option
+ parts := strings.SplitN(line, "=", 2)
+ keyName := parts[0]
+ if overrideVal, ok := overrides[strings.TrimSpace(keyName)]; ok {
+ // Override it
+ if overrideVal == "" {
+ fmt.Fprintf(outFile, "# %v is not set\n", keyName)
+ } else {
+ fmt.Fprintf(outFile, "%v=%v\n", keyName, overrideVal)
+ }
+ delete(overrides, keyName)
+ } else {
+ // Pass through unchanged
+ fmt.Fprintln(outFile, line)
+ }
+ }
+ }
+ // Process left over overrides
+ for key, val := range overrides {
+ fmt.Fprintf(outFile, "%v=%v\n", key, val)
+ }
+ return nil
+}
diff --git a/osbase/build/kconfig-patcher/main_test.go b/osbase/build/kconfig-patcher/main_test.go
new file mode 100644
index 0000000..11c7d84
--- /dev/null
+++ b/osbase/build/kconfig-patcher/main_test.go
@@ -0,0 +1,61 @@
+// 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 (
+ "bytes"
+ "strings"
+ "testing"
+)
+
+func Test_patchKconfig(t *testing.T) {
+ type args struct {
+ inFile string
+ overrides map[string]string
+ }
+ tests := []struct {
+ name string
+ args args
+ wantOutFile string
+ wantErr bool
+ }{
+ {
+ "passthroughExtend",
+ args{inFile: "# TEST=y\n\n", overrides: map[string]string{"TEST": "n"}},
+ "# TEST=y\n\nTEST=n\n",
+ false,
+ },
+ {
+ "patch",
+ args{inFile: "TEST=y\nTEST_NO=n\n", overrides: map[string]string{"TEST": "n"}},
+ "TEST=n\nTEST_NO=n\n",
+ false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ outFile := &bytes.Buffer{}
+ if err := patchKconfig(strings.NewReader(tt.args.inFile), outFile, tt.args.overrides); (err != nil) != tt.wantErr {
+ t.Errorf("patchKconfig() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if gotOutFile := outFile.String(); gotOutFile != tt.wantOutFile {
+ t.Errorf("patchKconfig() = %v, want %v", gotOutFile, tt.wantOutFile)
+ }
+ })
+ }
+}
diff --git a/osbase/build/mkcpio/BUILD.bazel b/osbase/build/mkcpio/BUILD.bazel
new file mode 100644
index 0000000..d281f6d
--- /dev/null
+++ b/osbase/build/mkcpio/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "mkcpio_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/osbase/build/mkcpio",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//osbase/build/fsspec",
+ "@com_github_cavaliergopher_cpio//:cpio",
+ "@com_github_klauspost_compress//zstd",
+ "@org_golang_x_sys//unix",
+ ],
+)
+
+go_binary(
+ name = "mkcpio",
+ embed = [":mkcpio_lib"],
+ visibility = ["//visibility:public"],
+)
diff --git a/osbase/build/mkcpio/main.go b/osbase/build/mkcpio/main.go
new file mode 100644
index 0000000..4886c3c
--- /dev/null
+++ b/osbase/build/mkcpio/main.go
@@ -0,0 +1,218 @@
+package main
+
+import (
+ "flag"
+ "io"
+ "log"
+ "os"
+ "path"
+ "sort"
+ "strings"
+
+ "github.com/cavaliergopher/cpio"
+ "github.com/klauspost/compress/zstd"
+ "golang.org/x/sys/unix"
+
+ "source.monogon.dev/osbase/build/fsspec"
+)
+
+var (
+ outPath = flag.String("out", "", "Output file path")
+)
+
+type placeEnum int
+
+const (
+ // placeNone implies that currently nothing is placed at that path.
+ // Can be overridden by everything.
+ placeNone placeEnum = 0
+ // placeDirImplicit means that there is currently a implied directory
+ // at the given path. It can be overridden by (and only by) an explicit
+ // directory.
+ placeDirImplicit placeEnum = 1
+ // placeDirExplicit means that there is an explicit (i.e. specified by
+ // the FSSpec) directory at the given path. Nothing else can override
+ // this.
+ placeDirExplicit placeEnum = 2
+ // placeNonDir means that there is a file-type resource (i.e a file, symlink
+ // or special_file) at the given path. Nothing else can override this.
+ placeNonDir placeEnum = 3
+)
+
+// place represents the state a given canonical path is in during metadata
+// construction. Its zero value is { State: placeNone, Inode: nil }.
+type place struct {
+ State placeEnum
+ // Inode contains one of the types inside an FSSpec (e.g. *fsspec.File)
+ Inode interface{}
+}
+
+// Usage: -out <out-path.cpio.zst> fsspec-path...
+func main() {
+ flag.Parse()
+ outFile, err := os.Create(*outPath)
+ if err != nil {
+ log.Fatalf("Failed to open CPIO output file: %v", err)
+ }
+ defer outFile.Close()
+ compressedOut, err := zstd.NewWriter(outFile)
+ if err != nil {
+ log.Fatalf("While initializing zstd writer: %v", err)
+ }
+ defer compressedOut.Close()
+ cpioWriter := cpio.NewWriter(compressedOut)
+ defer cpioWriter.Close()
+
+ spec, err := fsspec.ReadMergeSpecs(flag.Args())
+ if err != nil {
+ log.Fatalf("failed to load specs: %v", err)
+ }
+
+ // Map of paths to metadata for validation & implicit directory injection
+ places := make(map[string]place)
+
+ // The idea behind this machinery is that we try to place all files and
+ // directories into a map while creating the required parent directories
+ // on-the-fly as implicit directories. Overriding an implicit directory
+ // with an explicit one is allowed thus the actual order in which this
+ // structure is created does not matter. All non-directories cannot be
+ // overridden anyways so their insertion order does not matter.
+ // This also has the job of validating the FSSpec structure, ensuring that
+ // there are no duplicate paths and that there is nothing placed below a
+ // non-directory.
+ var placeInode func(p string, isDir bool, inode interface{})
+ placeInode = func(p string, isDir bool, inode interface{}) {
+ cleanPath := path.Clean(p)
+ if !isDir {
+ if places[cleanPath].State != placeNone {
+ log.Fatalf("Invalid FSSpec: Duplicate Inode at %q", cleanPath)
+ }
+ places[cleanPath] = place{
+ State: placeNonDir,
+ Inode: inode,
+ }
+ } else {
+ switch places[cleanPath].State {
+ case placeNone:
+ if inode != nil {
+ places[cleanPath] = place{
+ State: placeDirExplicit,
+ Inode: inode,
+ }
+ } else {
+ places[cleanPath] = place{
+ State: placeDirImplicit,
+ Inode: &fsspec.Directory{Path: cleanPath, Mode: 0555},
+ }
+ }
+ case placeDirImplicit:
+ if inode != nil {
+ places[cleanPath] = place{
+ State: placeDirExplicit,
+ Inode: inode,
+ }
+ }
+ case placeDirExplicit:
+ if inode != nil {
+ log.Fatalf("Invalid FSSpec: Conflicting explicit directories at %v", cleanPath)
+ }
+ case placeNonDir:
+ log.Fatalf("Invalid FSSpec: Trying to place inode below non-directory at #{cleanPath}")
+ default:
+ panic("unhandled placeEnum value")
+ }
+ }
+ parentPath, _ := path.Split(p)
+ parentPath = path.Clean(parentPath)
+ if parentPath == "/" || parentPath == p {
+ return
+ }
+ placeInode(parentPath, true, nil)
+ }
+ for _, d := range spec.Directory {
+ placeInode(d.Path, true, d)
+ }
+ for _, f := range spec.File {
+ placeInode(f.Path, false, f)
+ }
+ for _, s := range spec.SymbolicLink {
+ placeInode(s.Path, false, s)
+ }
+ for _, s := range spec.SpecialFile {
+ placeInode(s.Path, false, s)
+ }
+
+ var writeOrder []string
+ for path := range places {
+ writeOrder = append(writeOrder, path)
+ }
+ // Sorting a list of normalized paths representing a tree gives us Depth-
+ // first search (DFS) order which is the correct order for writing archives.
+ // This also makes the output reproducible.
+ sort.Strings(writeOrder)
+
+ for _, path := range writeOrder {
+ place := places[path]
+ switch i := place.Inode.(type) {
+ case *fsspec.File:
+ inF, err := os.Open(i.SourcePath)
+ if err != nil {
+ log.Fatalf("Failed to open source path for file %q: %v", i.Path, err)
+ }
+ inFStat, err := inF.Stat()
+ if err != nil {
+ log.Fatalf("Failed to stat source path for file %q: %v", i.Path, err)
+ }
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ Mode: cpio.FileMode(i.Mode),
+ Name: strings.TrimPrefix(i.Path, "/"),
+ Size: inFStat.Size(),
+ }); err != nil {
+ log.Fatalf("Failed to write cpio header for file %q: %v", i.Path, err)
+ }
+ if n, err := io.Copy(cpioWriter, inF); err != nil || n != inFStat.Size() {
+ log.Fatalf("Failed to copy file %q into cpio: %v", i.SourcePath, err)
+ }
+ inF.Close()
+ case *fsspec.Directory:
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ Mode: cpio.FileMode(i.Mode) | cpio.TypeDir,
+ Name: strings.TrimPrefix(i.Path, "/"),
+ }); err != nil {
+ log.Fatalf("Failed to write cpio header for directory %q: %v", i.Path, err)
+ }
+ case *fsspec.SymbolicLink:
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ // Symlinks are 0777 by definition (from man 7 symlink on Linux)
+ Mode: 0777 | cpio.TypeSymlink,
+ Name: strings.TrimPrefix(i.Path, "/"),
+ Size: int64(len(i.TargetPath)),
+ }); err != nil {
+ log.Fatalf("Failed to write cpio header for symlink %q: %v", i.Path, err)
+ }
+ if _, err := cpioWriter.Write([]byte(i.TargetPath)); err != nil {
+ log.Fatalf("Failed to write cpio symlink %q: %v", i.Path, err)
+ }
+ case *fsspec.SpecialFile:
+ mode := cpio.FileMode(i.Mode)
+ switch i.Type {
+ case fsspec.SpecialFile_CHARACTER_DEV:
+ mode |= cpio.TypeChar
+ case fsspec.SpecialFile_BLOCK_DEV:
+ mode |= cpio.TypeBlock
+ case fsspec.SpecialFile_FIFO:
+ mode |= cpio.TypeFifo
+ }
+
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ Mode: mode,
+ Name: strings.TrimPrefix(i.Path, "/"),
+ DeviceID: int(unix.Mkdev(i.Major, i.Minor)),
+ }); err != nil {
+ log.Fatalf("Failed to write CPIO header for special file %q: %v", i.Path, err)
+ }
+ default:
+ panic("inode type not handled")
+ }
+ }
+}
diff --git a/osbase/build/mkerofs/BUILD.bazel b/osbase/build/mkerofs/BUILD.bazel
new file mode 100644
index 0000000..ce648b9
--- /dev/null
+++ b/osbase/build/mkerofs/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "mkerofs_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/osbase/build/mkerofs",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//osbase/build/fsspec",
+ "//osbase/erofs",
+ ],
+)
+
+go_binary(
+ name = "mkerofs",
+ embed = [":mkerofs_lib"],
+ visibility = ["//visibility:public"],
+)
diff --git a/osbase/build/mkerofs/main.go b/osbase/build/mkerofs/main.go
new file mode 100644
index 0000000..edcfdb9
--- /dev/null
+++ b/osbase/build/mkerofs/main.go
@@ -0,0 +1,208 @@
+// 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.
+
+// mkerofs takes a specification in the form of a prototext file (see fsspec
+// next to this) and assembles an EROFS filesystem according to it. The output
+// is fully reproducible.
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "path"
+ "sort"
+ "strings"
+
+ "source.monogon.dev/osbase/build/fsspec"
+ "source.monogon.dev/osbase/erofs"
+)
+
+func (spec *entrySpec) writeRecursive(w *erofs.Writer, pathname string) {
+ switch inode := spec.data.Type.(type) {
+ case *fsspec.Inode_Directory:
+ // Sort children for reproducibility
+ var sortedChildren []string
+ for name := range spec.children {
+ sortedChildren = append(sortedChildren, name)
+ }
+ sort.Strings(sortedChildren)
+
+ err := w.Create(pathname, &erofs.Directory{
+ Base: erofs.Base{
+ Permissions: uint16(inode.Directory.Mode),
+ UID: uint16(inode.Directory.Uid),
+ GID: uint16(inode.Directory.Gid),
+ },
+ Children: sortedChildren,
+ })
+ if err != nil {
+ log.Fatalf("failed to write directory: %s", err)
+ }
+ for _, name := range sortedChildren {
+ spec.children[name].writeRecursive(w, path.Join(pathname, name))
+ }
+ case *fsspec.Inode_File:
+ iw := w.CreateFile(pathname, &erofs.FileMeta{
+ Base: erofs.Base{
+ Permissions: uint16(inode.File.Mode),
+ UID: uint16(inode.File.Uid),
+ GID: uint16(inode.File.Gid),
+ },
+ })
+
+ sourceFile, err := os.Open(inode.File.SourcePath)
+ if err != nil {
+ log.Fatalf("failed to open source file %s: %s", inode.File.SourcePath, err)
+ }
+
+ _, err = io.Copy(iw, sourceFile)
+ if err != nil {
+ log.Fatalf("failed to copy file into filesystem: %s", err)
+ }
+ sourceFile.Close()
+ if err := iw.Close(); err != nil {
+ log.Fatalf("failed to close target file: %s", err)
+ }
+ case *fsspec.Inode_SymbolicLink:
+ err := w.Create(pathname, &erofs.SymbolicLink{
+ Base: erofs.Base{
+ Permissions: 0777, // Nominal, Linux forces that mode anyways, see symlink(7)
+ },
+ Target: inode.SymbolicLink.TargetPath,
+ })
+ if err != nil {
+ log.Fatalf("failed to create symbolic link: %s", err)
+ }
+ case *fsspec.Inode_SpecialFile:
+ err := fmt.Errorf("unimplemented special file type %s", inode.SpecialFile.Type)
+ base := erofs.Base{
+ Permissions: uint16(inode.SpecialFile.Mode),
+ UID: uint16(inode.SpecialFile.Uid),
+ GID: uint16(inode.SpecialFile.Gid),
+ }
+ switch inode.SpecialFile.Type {
+ case fsspec.SpecialFile_FIFO:
+ err = w.Create(pathname, &erofs.FIFO{
+ Base: base,
+ })
+ case fsspec.SpecialFile_CHARACTER_DEV:
+ err = w.Create(pathname, &erofs.CharacterDevice{
+ Base: base,
+ Major: inode.SpecialFile.Major,
+ Minor: inode.SpecialFile.Minor,
+ })
+ case fsspec.SpecialFile_BLOCK_DEV:
+ err = w.Create(pathname, &erofs.BlockDevice{
+ Base: base,
+ Major: inode.SpecialFile.Major,
+ Minor: inode.SpecialFile.Minor,
+ })
+ }
+ if err != nil {
+ log.Fatalf("failed to make special file: %v", err)
+ }
+ }
+}
+
+// entrySpec is a recursive structure representing the filesystem tree
+type entrySpec struct {
+ data fsspec.Inode
+ children map[string]*entrySpec
+}
+
+// pathRef gets the entrySpec at the leaf of the given path, inferring
+// directories if necessary
+func (spec *entrySpec) pathRef(p string) *entrySpec {
+ // This block gets a path array starting at the root of the filesystem. The
+ // root folder is the zero-length array.
+ pathParts := strings.Split(path.Clean("./"+p), "/")
+ if pathParts[0] == "." {
+ pathParts = pathParts[1:]
+ }
+
+ entryRef := spec
+ for _, part := range pathParts {
+ childRef, ok := entryRef.children[part]
+ if !ok {
+ childRef = &entrySpec{
+ data: fsspec.Inode{Type: &fsspec.Inode_Directory{Directory: &fsspec.Directory{Mode: 0555}}},
+ children: make(map[string]*entrySpec),
+ }
+ entryRef.children[part] = childRef
+ }
+ entryRef = childRef
+ }
+ return entryRef
+}
+
+var (
+ outPath = flag.String("out", "", "Output file path")
+)
+
+func main() {
+ flag.Parse()
+
+ spec, err := fsspec.ReadMergeSpecs(flag.Args())
+ if err != nil {
+ log.Fatalf("failed to load specs: %v", err)
+ }
+
+ var fsRoot = &entrySpec{
+ data: fsspec.Inode{Type: &fsspec.Inode_Directory{Directory: &fsspec.Directory{Mode: 0555}}},
+ children: make(map[string]*entrySpec),
+ }
+
+ for _, dir := range spec.Directory {
+ entryRef := fsRoot.pathRef(dir.Path)
+ entryRef.data.Type = &fsspec.Inode_Directory{Directory: dir}
+ }
+
+ for _, file := range spec.File {
+ entryRef := fsRoot.pathRef(file.Path)
+ entryRef.data.Type = &fsspec.Inode_File{File: file}
+ }
+
+ for _, symlink := range spec.SymbolicLink {
+ entryRef := fsRoot.pathRef(symlink.Path)
+ entryRef.data.Type = &fsspec.Inode_SymbolicLink{SymbolicLink: symlink}
+ }
+
+ for _, specialFile := range spec.SpecialFile {
+ entryRef := fsRoot.pathRef(specialFile.Path)
+ entryRef.data.Type = &fsspec.Inode_SpecialFile{SpecialFile: specialFile}
+ }
+
+ fs, err := os.Create(*outPath)
+ if err != nil {
+ log.Fatalf("failed to open output file: %v", err)
+ }
+ writer, err := erofs.NewWriter(fs)
+ if err != nil {
+ log.Fatalf("failed to initialize EROFS writer: %v", err)
+ }
+
+ fsRoot.writeRecursive(writer, ".")
+
+ if err := writer.Close(); err != nil {
+ panic(err)
+ }
+ if err := fs.Close(); err != nil {
+ panic(err)
+ }
+}
diff --git a/osbase/build/mkimage/BUILD.bazel b/osbase/build/mkimage/BUILD.bazel
new file mode 100644
index 0000000..30ba81f
--- /dev/null
+++ b/osbase/build/mkimage/BUILD.bazel
@@ -0,0 +1,22 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "mkimage_lib",
+ srcs = ["main.go"],
+ embedsrcs = [
+ "//metropolis/node/core/abloader", #keep
+ ],
+ importpath = "source.monogon.dev/osbase/build/mkimage",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//osbase/blkio",
+ "//osbase/blockdev",
+ "//osbase/build/mkimage/osimage",
+ ],
+)
+
+go_binary(
+ name = "mkimage",
+ embed = [":mkimage_lib"],
+ visibility = ["//metropolis/node:__pkg__"],
+)
diff --git a/osbase/build/mkimage/def.bzl b/osbase/build/mkimage/def.bzl
new file mode 100644
index 0000000..1663e3d
--- /dev/null
+++ b/osbase/build/mkimage/def.bzl
@@ -0,0 +1,48 @@
+def _node_image_impl(ctx):
+ img_file = ctx.actions.declare_file(ctx.label.name + ".img")
+ ctx.actions.run(
+ mnemonic = "MkImage",
+ executable = ctx.executable._mkimage,
+ arguments = [
+ "-efi",
+ ctx.file.kernel.path,
+ "-system",
+ ctx.file.system.path,
+ "-out",
+ img_file.path,
+ ],
+ inputs = [
+ ctx.file.kernel,
+ ctx.file.system,
+ ],
+ outputs = [img_file],
+ )
+
+ return [DefaultInfo(files = depset([img_file]), runfiles = ctx.runfiles(files = [img_file]))]
+
+node_image = rule(
+ implementation = _node_image_impl,
+ doc = """
+ Build a disk image from an EFI kernel payload and system partition
+ contents. See //osbase/build/mkimage for more information.
+ """,
+ attrs = {
+ "kernel": attr.label(
+ doc = "EFI binary containing a kernel.",
+ mandatory = True,
+ allow_single_file = True,
+ ),
+ "system": attr.label(
+ doc = "Contents of the system partition.",
+ mandatory = True,
+ allow_single_file = True,
+ ),
+ "_mkimage": attr.label(
+ doc = "The mkimage executable.",
+ default = "//osbase/build/mkimage",
+ allow_single_file = True,
+ executable = True,
+ cfg = "exec",
+ ),
+ },
+)
diff --git a/osbase/build/mkimage/main.go b/osbase/build/mkimage/main.go
new file mode 100644
index 0000000..d83a03d
--- /dev/null
+++ b/osbase/build/mkimage/main.go
@@ -0,0 +1,106 @@
+// 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.
+
+// mkimage is a tool to generate node disk images.
+// It can be used both to initialize block devices and to create image
+// files.
+//
+// The tool takes a path to an EFI payload (--efi), and a path to a
+// system image (--system) as its only required inputs. In
+// addition, an output path must be supplied (--out).
+// Node parameters file path (--node_parameters) may also be supplied, in
+// which case the file will be copied to the EFI system partition.
+// Partition sizes are fixed and may be overridden by command line flags.
+package main
+
+import (
+ "bytes"
+ _ "embed"
+ "flag"
+ "log"
+ "os"
+
+ "source.monogon.dev/osbase/blkio"
+ "source.monogon.dev/osbase/blockdev"
+ "source.monogon.dev/osbase/build/mkimage/osimage"
+)
+
+//go:embed metropolis/node/core/abloader/abloader_bin.efi
+var abloader []byte
+
+func main() {
+ // Fill in the image parameters based on flags.
+ var (
+ efiPayload string
+ systemImage string
+ nodeParams string
+ outputPath string
+ diskUUID string
+ cfg osimage.Params
+ )
+ flag.StringVar(&efiPayload, "efi", "", "Path to the UEFI payload used")
+ flag.StringVar(&systemImage, "system", "", "Path to the system partition image used")
+ flag.StringVar(&nodeParams, "node_parameters", "", "Path to Node Parameters to be written to the ESP (default: don't write Node Parameters)")
+ flag.StringVar(&outputPath, "out", "", "Path to the resulting disk image or block device")
+ flag.Int64Var(&cfg.PartitionSize.Data, "data_partition_size", 2048, "Override the data partition size (default 2048 MiB). Used only when generating image files.")
+ flag.Int64Var(&cfg.PartitionSize.ESP, "esp_partition_size", 128, "Override the ESP partition size (default: 128MiB)")
+ flag.Int64Var(&cfg.PartitionSize.System, "system_partition_size", 1024, "Override the System partition size (default: 1024MiB)")
+ flag.StringVar(&diskUUID, "GUID", "", "Disk GUID marked in the resulting image's partition table (default: randomly generated)")
+ flag.Parse()
+
+ // Open the input files for osimage.Create, fill in reader objects and
+ // metadata in osimage.Params.
+ // Start with the EFI Payload the OS will boot from.
+ p, err := blkio.NewFileReader(efiPayload)
+ if err != nil {
+ log.Fatalf("while opening the EFI payload at %q: %v", efiPayload, err)
+ }
+ cfg.EFIPayload = p
+
+ // Attempt to open the system image if its path is set. In case the path
+ // isn't set, the system partition will still be created, but no
+ // contents will be written into it.
+ if systemImage != "" {
+ img, err := os.Open(systemImage)
+ if err != nil {
+ log.Fatalf("while opening the system image at %q: %v", systemImage, err)
+ }
+ defer img.Close()
+ cfg.SystemImage = img
+ }
+
+ // Attempt to open the node parameters file if its path is set.
+ if nodeParams != "" {
+ np, err := blkio.NewFileReader(nodeParams)
+ if err != nil {
+ log.Fatalf("while opening node parameters at %q: %v", nodeParams, err)
+ }
+ cfg.NodeParameters = np
+ }
+
+ // TODO(#254): Build and use dynamically-grown block devices
+ cfg.Output, err = blockdev.CreateFile(outputPath, 512, 10*1024*1024)
+ if err != nil {
+ panic(err)
+ }
+
+ cfg.ABLoader = bytes.NewReader(abloader)
+
+ // Write the parametrized OS image.
+ if _, err := osimage.Write(&cfg); err != nil {
+ log.Fatalf("while creating a Metropolis OS image: %v", err)
+ }
+}
diff --git a/osbase/build/mkimage/osimage/BUILD.bazel b/osbase/build/mkimage/osimage/BUILD.bazel
new file mode 100644
index 0000000..cfcf096
--- /dev/null
+++ b/osbase/build/mkimage/osimage/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "osimage",
+ srcs = ["osimage.go"],
+ importpath = "source.monogon.dev/osbase/build/mkimage/osimage",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//osbase/blockdev",
+ "//osbase/efivarfs",
+ "//osbase/fat32",
+ "//osbase/gpt",
+ "@com_github_google_uuid//:uuid",
+ ],
+)
diff --git a/osbase/build/mkimage/osimage/osimage.go b/osbase/build/mkimage/osimage/osimage.go
new file mode 100644
index 0000000..d139d6b
--- /dev/null
+++ b/osbase/build/mkimage/osimage/osimage.go
@@ -0,0 +1,237 @@
+// 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.
+
+// This package provides self-contained implementation used to generate
+// Metropolis disk images.
+package osimage
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/google/uuid"
+
+ "source.monogon.dev/osbase/blockdev"
+ "source.monogon.dev/osbase/efivarfs"
+ "source.monogon.dev/osbase/fat32"
+ "source.monogon.dev/osbase/gpt"
+)
+
+var (
+ SystemAType = uuid.MustParse("ee96054b-f6d0-4267-aaaa-724b2afea74c")
+ SystemBType = uuid.MustParse("ee96054b-f6d0-4267-bbbb-724b2afea74c")
+
+ DataType = uuid.MustParse("9eeec464-6885-414a-b278-4305c51f7966")
+)
+
+const (
+ SystemALabel = "METROPOLIS-SYSTEM-A"
+ SystemBLabel = "METROPOLIS-SYSTEM-B"
+ DataLabel = "METROPOLIS-NODE-DATA"
+ ESPLabel = "ESP"
+
+ EFIPayloadPath = "/EFI/BOOT/BOOTx64.EFI"
+ EFIBootAPath = "/EFI/metropolis/boot-a.efi"
+ EFIBootBPath = "/EFI/metropolis/boot-b.efi"
+ nodeParamsPath = "metropolis/parameters.pb"
+)
+
+// PartitionSizeInfo contains parameters used during partition table
+// initialization and, in case of image files, space allocation.
+type PartitionSizeInfo struct {
+ // Size of the EFI System Partition (ESP), in mebibytes. The size must
+ // not be zero.
+ ESP int64
+ // Size of the Metropolis system partition, in mebibytes. The partition
+ // won't be created if the size is zero.
+ System int64
+ // Size of the Metropolis data partition, in mebibytes. The partition
+ // won't be created if the size is zero. If the image is output to a
+ // block device, the partition will be extended to fill the remaining
+ // space.
+ Data int64
+}
+
+// Params contains parameters used by Plan or Write to build a Metropolis OS
+// image.
+type Params struct {
+ // Output is the block device to which the OS image is written.
+ Output blockdev.BlockDev
+ // ABLoader provides the A/B loader which then loads the EFI loader for the
+ // correct slot.
+ ABLoader fat32.SizedReader
+ // EFIPayload provides contents of the EFI payload file. It must not be
+ // nil. This gets put into boot slot A.
+ EFIPayload fat32.SizedReader
+ // SystemImage provides contents of the Metropolis system partition.
+ // If nil, no contents will be copied into the partition.
+ SystemImage io.Reader
+ // NodeParameters provides contents of the node parameters file. If nil,
+ // the node parameters file won't be created in the target ESP
+ // filesystem.
+ NodeParameters fat32.SizedReader
+ // DiskGUID is a unique identifier of the image and a part of Table
+ // header. It's optional and can be left blank if the identifier is
+ // to be randomly generated. Setting it to a predetermined value can
+ // help in implementing reproducible builds.
+ DiskGUID uuid.UUID
+ // PartitionSize specifies a size for the ESP, Metropolis System and
+ // Metropolis data partition.
+ PartitionSize PartitionSizeInfo
+}
+
+type plan struct {
+ *Params
+ rootInode fat32.Inode
+ tbl *gpt.Table
+ efiPartition *gpt.Partition
+ systemPartitionA *gpt.Partition
+ systemPartitionB *gpt.Partition
+ dataPartition *gpt.Partition
+}
+
+// Apply actually writes the planned installation to the blockdevice.
+func (i *plan) Apply() (*efivarfs.LoadOption, error) {
+ // Discard the entire device, we're going to write new data over it.
+ // Ignore errors, this is only advisory.
+ i.Output.Discard(0, i.Output.BlockCount()*i.Output.BlockSize())
+
+ if err := fat32.WriteFS(blockdev.NewRWS(i.efiPartition), i.rootInode, fat32.Options{
+ BlockSize: uint16(i.efiPartition.BlockSize()),
+ BlockCount: uint32(i.efiPartition.BlockCount()),
+ Label: "MNGN_BOOT",
+ }); err != nil {
+ return nil, fmt.Errorf("failed to write FAT32: %w", err)
+ }
+
+ if _, err := io.Copy(blockdev.NewRWS(i.systemPartitionA), i.SystemImage); err != nil {
+ return nil, fmt.Errorf("failed to write system partition A: %w", err)
+ }
+
+ if err := i.tbl.Write(); err != nil {
+ return nil, fmt.Errorf("failed to write Table: %w", err)
+ }
+
+ // Build an EFI boot entry pointing to the image's ESP.
+ return &efivarfs.LoadOption{
+ Description: "Metropolis",
+ FilePath: efivarfs.DevicePath{
+ &efivarfs.HardDrivePath{
+ PartitionNumber: 1,
+ PartitionStartBlock: i.efiPartition.FirstBlock,
+ PartitionSizeBlocks: i.efiPartition.SizeBlocks(),
+ PartitionMatch: efivarfs.PartitionGPT{
+ PartitionUUID: i.efiPartition.ID,
+ },
+ },
+ efivarfs.FilePath(EFIPayloadPath),
+ },
+ }, nil
+}
+
+// Plan allows to prepare an installation without modifying any data on the
+// system. To apply the planned installation, call Apply on the returned plan.
+func Plan(p *Params) (*plan, error) {
+ params := &plan{Params: p}
+
+ var err error
+ params.tbl, err = gpt.New(params.Output)
+ if err != nil {
+ return nil, fmt.Errorf("invalid block device: %w", err)
+ }
+
+ params.tbl.ID = params.DiskGUID
+ params.efiPartition = &gpt.Partition{
+ Type: gpt.PartitionTypeEFISystem,
+ Name: ESPLabel,
+ }
+
+ if err := params.tbl.AddPartition(params.efiPartition, params.PartitionSize.ESP*Mi); err != nil {
+ return nil, fmt.Errorf("failed to allocate ESP: %w", err)
+ }
+
+ params.rootInode = fat32.Inode{
+ Attrs: fat32.AttrDirectory,
+ }
+ if err := params.rootInode.PlaceFile(strings.TrimPrefix(EFIBootAPath, "/"), params.EFIPayload); err != nil {
+ return nil, err
+ }
+ // Place the A/B loader at the EFI bootloader autodiscovery path.
+ if err := params.rootInode.PlaceFile(strings.TrimPrefix(EFIPayloadPath, "/"), params.ABLoader); err != nil {
+ return nil, err
+ }
+ if params.NodeParameters != nil {
+ if err := params.rootInode.PlaceFile(nodeParamsPath, params.NodeParameters); err != nil {
+ return nil, err
+ }
+ }
+
+ // Try to layout the fat32 partition. If it detects that the disk is too
+ // small, an error will be returned.
+ if _, err := fat32.SizeFS(params.rootInode, fat32.Options{
+ BlockSize: uint16(params.efiPartition.BlockSize()),
+ BlockCount: uint32(params.efiPartition.BlockCount()),
+ Label: "MNGN_BOOT",
+ }); err != nil {
+ return nil, fmt.Errorf("failed to calculate size of FAT32: %w", err)
+ }
+
+ // Create the system partition only if its size is specified.
+ if params.PartitionSize.System != 0 && params.SystemImage != nil {
+ params.systemPartitionA = &gpt.Partition{
+ Type: SystemAType,
+ Name: SystemALabel,
+ }
+ if err := params.tbl.AddPartition(params.systemPartitionA, params.PartitionSize.System*Mi); err != nil {
+ return nil, fmt.Errorf("failed to allocate system partition A: %w", err)
+ }
+ params.systemPartitionB = &gpt.Partition{
+ Type: SystemBType,
+ Name: SystemBLabel,
+ }
+ if err := params.tbl.AddPartition(params.systemPartitionB, params.PartitionSize.System*Mi); err != nil {
+ return nil, fmt.Errorf("failed to allocate system partition B: %w", err)
+ }
+ } else if params.PartitionSize.System == 0 && params.SystemImage != nil {
+ // Safeguard against contradicting parameters.
+ return nil, fmt.Errorf("the system image parameter was passed while the associated partition size is zero")
+ }
+ // Create the data partition only if its size is specified.
+ if params.PartitionSize.Data != 0 {
+ params.dataPartition = &gpt.Partition{
+ Type: DataType,
+ Name: DataLabel,
+ }
+ if err := params.tbl.AddPartition(params.dataPartition, -1); err != nil {
+ return nil, fmt.Errorf("failed to allocate data partition: %w", err)
+ }
+ }
+
+ return params, nil
+}
+
+const Mi = 1024 * 1024
+
+// Write writes a Metropolis OS image to a block device.
+func Write(params *Params) (*efivarfs.LoadOption, error) {
+ p, err := Plan(params)
+ if err != nil {
+ return nil, err
+ }
+
+ return p.Apply()
+}
diff --git a/osbase/build/mkpayload/BUILD.bazel b/osbase/build/mkpayload/BUILD.bazel
new file mode 100644
index 0000000..7e49388
--- /dev/null
+++ b/osbase/build/mkpayload/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_binary(
+ name = "mkpayload",
+ embed = [":mkpayload_lib"],
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "mkpayload_lib",
+ srcs = ["mkpayload.go"],
+ importpath = "source.monogon.dev/osbase/build/mkpayload",
+ visibility = ["//visibility:private"],
+)
diff --git a/osbase/build/mkpayload/mkpayload.go b/osbase/build/mkpayload/mkpayload.go
new file mode 100644
index 0000000..a66d458
--- /dev/null
+++ b/osbase/build/mkpayload/mkpayload.go
@@ -0,0 +1,188 @@
+// 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.
+
+// mkpayload is an objcopy wrapper that builds EFI unified kernel images. It
+// performs actions that can't be realized by either objcopy or the
+// buildsystem.
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "os/exec"
+ "strings"
+)
+
+type stringList []string
+
+func (l *stringList) String() string {
+ if l == nil {
+ return ""
+ }
+ return strings.Join(*l, ", ")
+}
+
+func (l *stringList) Set(value string) error {
+ *l = append(*l, value)
+ return nil
+}
+
+var (
+ // sections contains VMAs and source files of the payload PE sections. The
+ // file path pointers will be filled in when the flags are parsed. It's used
+ // to generate objcopy command line arguments. Entries that are "required"
+ // will cause the program to stop and print usage information if not provided
+ // as command line parameters.
+ sections = map[string]struct {
+ descr string
+ vma string
+ required bool
+ file *string
+ }{
+ "linux": {"Linux kernel image", "0x2000000", true, nil},
+ "initrd": {"initramfs", "0x5000000", false, nil},
+ "osrel": {"OS release file in text format", "0x20000", false, nil},
+ "cmdline": {"a file containting additional kernel command line parameters", "0x30000", false, nil},
+ "splash": {"a splash screen image in BMP format", "0x40000", false, nil},
+ }
+ initrdList stringList
+ objcopy = flag.String("objcopy", "", "objcopy executable")
+ stub = flag.String("stub", "", "the EFI stub executable")
+ output = flag.String("output", "", "objcopy output")
+ rootfs_dm_table = flag.String("rootfs_dm_table", "", "a text file containing the DeviceMapper rootfs target table")
+)
+
+func main() {
+ flag.Var(&initrdList, "initrd", "Path to initramfs, can be given multiple times")
+ // Register parameters related to the EFI payload sections, then parse the flags.
+ for k, v := range sections {
+ if k == "initrd" { // initrd is special because it accepts multiple payloads
+ continue
+ }
+ v.file = flag.String(k, "", v.descr)
+ sections[k] = v
+ }
+ flag.Parse()
+
+ // Ensure all the required parameters are filled in.
+ for n, s := range sections {
+ if s.required && *s.file == "" {
+ log.Fatalf("-%s parameter is missing.", n)
+ }
+ }
+ if *objcopy == "" {
+ log.Fatalf("-objcopy parameter is missing.")
+ }
+ if *stub == "" {
+ log.Fatalf("-stub parameter is missing.")
+ }
+ if *output == "" {
+ log.Fatalf("-output parameter is missing.")
+ }
+
+ // If a DeviceMapper table was passed, configure the kernel to boot from the
+ // device described by it, while keeping any other kernel command line
+ // parameters that might have been passed through "-cmdline".
+ if *rootfs_dm_table != "" {
+ var cmdline string
+ p := *sections["cmdline"].file
+ if p != "" {
+ c, err := os.ReadFile(p)
+ if err != nil {
+ log.Fatalf("%v", err)
+ }
+ cmdline = string(c[:])
+
+ if strings.Contains(cmdline, "root=") {
+ log.Fatalf("A DeviceMapper table was passed, however the kernel command line already contains a \"root=\" statement.")
+ }
+ }
+
+ vt, err := os.ReadFile(*rootfs_dm_table)
+ if err != nil {
+ log.Fatalf("%v", err)
+ }
+ cmdline += fmt.Sprintf(" dm-mod.create=\"rootfs,,,ro,%s\" root=/dev/dm-0", vt)
+
+ out, err := os.CreateTemp(".", "cmdline")
+ if err != nil {
+ log.Fatalf("%v", err)
+ }
+ defer os.Remove(out.Name())
+ if _, err = out.Write([]byte(cmdline[:])); err != nil {
+ log.Fatalf("%v", err)
+ }
+ out.Close()
+
+ *sections["cmdline"].file = out.Name()
+ }
+
+ var initrdPath string
+ if len(initrdList) > 0 {
+ initrd, err := os.CreateTemp(".", "initrd")
+ if err != nil {
+ log.Fatalf("Failed to create temporary initrd: %v", err)
+ }
+ defer os.Remove(initrd.Name())
+ for _, initrdPath := range initrdList {
+ initrdSrc, err := os.Open(initrdPath)
+ if err != nil {
+ log.Fatalf("Failed to open initrd file: %v", err)
+ }
+ if _, err := io.Copy(initrd, initrdSrc); err != nil {
+ initrdSrc.Close()
+ log.Fatalf("Failed concatinating initrd: %v", err)
+ }
+ initrdSrc.Close()
+ }
+ initrdPath = initrd.Name()
+ }
+ sec := sections["initrd"]
+ sec.file = &initrdPath
+ sections["initrd"] = sec
+
+ // Execute objcopy
+ var args []string
+ for name, c := range sections {
+ if *c.file != "" {
+ args = append(args, []string{
+ "--add-section", fmt.Sprintf(".%s=%s", name, *c.file),
+ "--change-section-vma", fmt.Sprintf(".%s=%s", name, c.vma),
+ }...)
+ }
+ }
+ args = append(args, []string{
+ *stub,
+ *output,
+ }...)
+ cmd := exec.Command(*objcopy, args...)
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+ err := cmd.Run()
+ if err == nil {
+ return
+ }
+ // Exit with objcopy's return code.
+ var e *exec.ExitError
+ if errors.As(err, &e) {
+ os.Exit(e.ExitCode())
+ }
+ log.Fatalf("Could not start command: %v", err)
+}
diff --git a/osbase/build/mkucode/BUILD.bazel b/osbase/build/mkucode/BUILD.bazel
new file mode 100644
index 0000000..9f56e10
--- /dev/null
+++ b/osbase/build/mkucode/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "mkucode_lib",
+ srcs = ["main.go"],
+ importpath = "source.monogon.dev/osbase/build/mkucode",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//osbase/build/mkucode/spec",
+ "@com_github_cavaliergopher_cpio//:cpio",
+ "@org_golang_google_protobuf//encoding/prototext",
+ ],
+)
+
+go_binary(
+ name = "mkucode",
+ embed = [":mkucode_lib"],
+ visibility = ["//visibility:public"],
+)
diff --git a/osbase/build/mkucode/def.bzl b/osbase/build/mkucode/def.bzl
new file mode 100644
index 0000000..04d83a2
--- /dev/null
+++ b/osbase/build/mkucode/def.bzl
@@ -0,0 +1,45 @@
+def _cpio_ucode_impl(ctx):
+ ucode_spec = ctx.actions.declare_file(ctx.label.name + "_spec.prototxt")
+
+ vendors = []
+ inputs = []
+ for label, vendor in ctx.attr.ucode.items():
+ files = label[DefaultInfo].files.to_list()
+ inputs += files
+ vendors.append(struct(id = vendor, file = [f.path for f in files]))
+
+ ctx.actions.write(ucode_spec, proto.encode_text(struct(vendor = vendors)))
+
+ output_file = ctx.actions.declare_file(ctx.label.name + ".cpio")
+ ctx.actions.run(
+ outputs = [output_file],
+ inputs = [ucode_spec] + inputs,
+ tools = [ctx.executable._mkucode],
+ executable = ctx.executable._mkucode,
+ arguments = ["-out", output_file.path, "-spec", ucode_spec.path],
+ )
+ return [DefaultInfo(files = depset([output_file]))]
+
+cpio_ucode = rule(
+ implementation = _cpio_ucode_impl,
+ doc = """
+ Builds a cpio archive with microcode for the Linux early microcode loader.
+ """,
+ attrs = {
+ "ucode": attr.label_keyed_string_dict(
+ mandatory = True,
+ allow_files = True,
+ doc = """
+ Dictionary of Labels to String. Each label is a list of microcode files and the string label
+ is the vendor ID corresponding to that microcode.
+ """,
+ ),
+
+ # Tool
+ "_mkucode": attr.label(
+ default = Label("//osbase/build/mkucode"),
+ executable = True,
+ cfg = "exec",
+ ),
+ },
+)
diff --git a/osbase/build/mkucode/main.go b/osbase/build/mkucode/main.go
new file mode 100644
index 0000000..58561e3
--- /dev/null
+++ b/osbase/build/mkucode/main.go
@@ -0,0 +1,71 @@
+// This assembles standalone microcode files into the format expected by the
+// Linux microcode loader. See
+// https://www.kernel.org/doc/html/latest/x86/microcode.html for further
+// information.
+package main
+
+import (
+ "flag"
+ "io"
+ "log"
+ "os"
+
+ "github.com/cavaliergopher/cpio"
+ "google.golang.org/protobuf/encoding/prototext"
+
+ "source.monogon.dev/osbase/build/mkucode/spec"
+)
+
+var (
+ specPath = flag.String("spec", "", "Path to prototext specification (osbase.build.mkucode.UCode)")
+ outPath = flag.String("out", "", "Output path for cpio to be prepend to initrd")
+)
+
+// Usage: -spec <ucode.prototxt> -out <ucode.cpio>
+func main() {
+ flag.Parse()
+ specRaw, err := os.ReadFile(*specPath)
+ if err != nil {
+ log.Fatalf("Failed to read spec: %v", err)
+ }
+ var ucodeSpec spec.UCode
+ if err := prototext.Unmarshal(specRaw, &ucodeSpec); err != nil {
+ log.Fatalf("Failed unmarshaling ucode spec: %v", err)
+ }
+ out, err := os.Create(*outPath)
+ if err != nil {
+ log.Fatalf("Failed to create cpio: %v", err)
+ }
+ defer out.Close()
+ cpioWriter := cpio.NewWriter(out)
+ for _, vendor := range ucodeSpec.Vendor {
+ var totalSize int64
+ for _, file := range vendor.File {
+ data, err := os.Stat(file)
+ if err != nil {
+ log.Fatalf("Failed to stat file for vendor %q: %v", vendor.Id, err)
+ }
+ totalSize += data.Size()
+ }
+ if err := cpioWriter.WriteHeader(&cpio.Header{
+ Mode: 0444,
+ Name: "kernel/x86/microcode/" + vendor.Id + ".bin",
+ Size: totalSize,
+ }); err != nil {
+ log.Fatalf("Failed to write cpio header for vendor %q: %v", vendor.Id, err)
+ }
+ for _, file := range vendor.File {
+ f, err := os.Open(file)
+ if err != nil {
+ log.Fatalf("Failed to open file for vendor %q: %v", vendor.Id, err)
+ }
+ if _, err := io.Copy(cpioWriter, f); err != nil {
+ log.Fatalf("Failed to copy data for file %q: %v", file, err)
+ }
+ f.Close()
+ }
+ }
+ if err := cpioWriter.Close(); err != nil {
+ log.Fatalf("Failed writing cpio: %v", err)
+ }
+}
diff --git a/osbase/build/mkucode/spec/BUILD.bazel b/osbase/build/mkucode/spec/BUILD.bazel
new file mode 100644
index 0000000..0210a4b
--- /dev/null
+++ b/osbase/build/mkucode/spec/BUILD.bazel
@@ -0,0 +1,30 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+ name = "mkucode_proto",
+ srcs = ["spec.proto"],
+ visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+ name = "mkucode_go_proto",
+ importpath = "source.monogon.dev/osbase/build/mkucode",
+ proto = ":mkucode_proto",
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "spec",
+ embed = [":spec_go_proto"],
+ importpath = "source.monogon.dev/osbase/build/mkucode/spec",
+ visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+ name = "spec_go_proto",
+ importpath = "source.monogon.dev/osbase/build/mkucode/spec",
+ proto = ":mkucode_proto",
+ visibility = ["//visibility:public"],
+)
diff --git a/osbase/build/mkucode/spec/gomod-generated-placeholder.go b/osbase/build/mkucode/spec/gomod-generated-placeholder.go
new file mode 100644
index 0000000..f09cd57
--- /dev/null
+++ b/osbase/build/mkucode/spec/gomod-generated-placeholder.go
@@ -0,0 +1 @@
+package spec
diff --git a/osbase/build/mkucode/spec/spec.proto b/osbase/build/mkucode/spec/spec.proto
new file mode 100644
index 0000000..36fcc4c
--- /dev/null
+++ b/osbase/build/mkucode/spec/spec.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+package osbase.build.mkucode;
+option go_package = "source.monogon.dev/osbase/build/mkucode/spec";
+
+message UCode {
+ repeated UCodeVendor vendor = 1;
+}
+
+message UCodeVendor {
+ // The vendor id (as given in cpuid) of the CPU the microcode is for, like
+ // GenuineIntel or AuthenticAMD.
+ string id = 1;
+
+ // List of paths to microcode files from for CPUs from the vendor.
+ repeated string file = 2;
+}
\ No newline at end of file
diff --git a/osbase/build/mkverity/BUILD.bazel b/osbase/build/mkverity/BUILD.bazel
new file mode 100644
index 0000000..04a8a54
--- /dev/null
+++ b/osbase/build/mkverity/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_binary(
+ name = "mkverity",
+ embed = [":mkverity_lib"],
+ visibility = [
+ "//metropolis/installer/test/testos:__pkg__",
+ "//metropolis/node:__pkg__",
+ "//metropolis/node/core/update/e2e/testos:__pkg__",
+ ],
+)
+
+go_library(
+ name = "mkverity_lib",
+ srcs = ["mkverity.go"],
+ importpath = "source.monogon.dev/osbase/build/mkverity",
+ visibility = ["//visibility:private"],
+ deps = ["//osbase/verity"],
+)
diff --git a/osbase/build/mkverity/mkverity.go b/osbase/build/mkverity/mkverity.go
new file mode 100644
index 0000000..ff2807b
--- /dev/null
+++ b/osbase/build/mkverity/mkverity.go
@@ -0,0 +1,152 @@
+// 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.
+
+// This package implements a command line tool that creates dm-verity hash
+// images at a selected path, given an existing data image. The tool
+// outputs a Verity mapping table on success.
+//
+// For more information, see:
+// - source.monogon.dev/osbase/verity
+// - https://gitlab.com/cryptsetup/cryptsetup/wikis/DMVerity
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+
+ "source.monogon.dev/osbase/verity"
+)
+
+// createImage creates a dm-verity target image by combining the input image
+// with Verity metadata. Contents of the data image are copied to the output
+// image. Then, the same contents are verity-encoded and appended to the
+// output image. The verity superblock is written only if wsb is true. It
+// returns either a dm-verity target table, or an error.
+func createImage(dataImagePath, outputImagePath string, wsb bool) (*verity.MappingTable, error) {
+ // Hardcode both the data block size and the hash block size as 4096 bytes.
+ bs := uint32(4096)
+
+ // Open the data image for reading.
+ dataImage, err := os.Open(dataImagePath)
+ if err != nil {
+ return nil, fmt.Errorf("while opening the data image: %w", err)
+ }
+ defer dataImage.Close()
+
+ // Check that the data image is well-formed.
+ ds, err := dataImage.Stat()
+ if err != nil {
+ return nil, fmt.Errorf("while stat-ing the data image: %w", err)
+ }
+ if !ds.Mode().IsRegular() {
+ return nil, fmt.Errorf("the data image must be a regular file")
+ }
+ if ds.Size()%int64(bs) != 0 {
+ return nil, fmt.Errorf("the data image must end on a %d-byte block boundary", bs)
+ }
+
+ // Create an empty hash image file.
+ outputImage, err := os.OpenFile(outputImagePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
+ if err != nil {
+ return nil, fmt.Errorf("while opening the hash image for writing: %w", err)
+ }
+ defer outputImage.Close()
+
+ // Copy the input data into the output file, then rewind dataImage to be read
+ // again by the Verity encoder.
+ _, err = io.Copy(outputImage, dataImage)
+ if err != nil {
+ return nil, err
+ }
+ _, err = dataImage.Seek(0, os.SEEK_SET)
+ if err != nil {
+ return nil, err
+ }
+
+ // Write outputImage contents. Start with initializing a verity encoder,
+ // seting outputImage as its output.
+ v, err := verity.NewEncoder(outputImage, bs, bs, wsb)
+ if err != nil {
+ return nil, fmt.Errorf("while initializing a verity encoder: %w", err)
+ }
+ // Hash the contents of dataImage, block by block.
+ _, err = io.Copy(v, dataImage)
+ if err != nil {
+ return nil, fmt.Errorf("while reading the data image: %w", err)
+ }
+ // The resulting hash tree won't be written until Close is called.
+ err = v.Close()
+ if err != nil {
+ return nil, fmt.Errorf("while writing the hash image: %w", err)
+ }
+
+ // Return an encoder-generated verity mapping table, containing the salt and
+ // the root hash. First, calculate the starting hash block by dividing the
+ // data image size by the encoder data block size.
+ hashStart := ds.Size() / int64(bs)
+ mt, err := v.MappingTable(dataImagePath, outputImagePath, hashStart)
+ if err != nil {
+ return nil, fmt.Errorf("while querying for the mapping table: %w", err)
+ }
+ return mt, nil
+}
+
+var (
+ input = flag.String("input", "", "input disk image (required)")
+ output = flag.String("output", "", "output disk image with Verity metadata appended (required)")
+ dataDeviceAlias = flag.String("data_alias", "", "data device alias used in the mapping table")
+ hashDeviceAlias = flag.String("hash_alias", "", "hash device alias used in the mapping table")
+ table = flag.String("table", "", "a file the mapping table will be saved to; disables stdout")
+)
+
+func main() {
+ flag.Parse()
+
+ // Ensure that required parameters were provided before continuing.
+ if *input == "" {
+ log.Fatalf("-input must be set.")
+ }
+ if *output == "" {
+ log.Fatalf("-output must be set.")
+ }
+
+ // Build the image.
+ mt, err := createImage(*input, *output, false)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // Patch the device names, if alternatives were provided.
+ if *dataDeviceAlias != "" {
+ mt.DataDevicePath = *dataDeviceAlias
+ }
+ if *hashDeviceAlias != "" {
+ mt.HashDevicePath = *hashDeviceAlias
+ }
+
+ // Print a DeviceMapper target table, or save it to a file, if the table
+ // parameter was specified.
+ if *table != "" {
+ if err := os.WriteFile(*table, []byte(mt.String()), 0644); err != nil {
+ log.Fatal(err)
+ }
+ } else {
+ fmt.Println(mt)
+ }
+}