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/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