|  | #  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 _build_pure_transition_impl(settings, attr): | 
|  | """ | 
|  | Transition that enables pure, static build of Go binaries. | 
|  | """ | 
|  | return { | 
|  | "@io_bazel_rules_go//go/config:pure": True, | 
|  | "@io_bazel_rules_go//go/config:static": True, | 
|  | } | 
|  |  | 
|  | build_pure_transition = transition( | 
|  | implementation = _build_pure_transition_impl, | 
|  | inputs = [], | 
|  | 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:crosstool_top": "//build/toolchain/musl-host-gcc:musl_host_cc_suite", | 
|  | } | 
|  |  | 
|  | build_static_transition = transition( | 
|  | implementation = _build_static_transition_impl, | 
|  | inputs = [], | 
|  | outputs = [ | 
|  | "@io_bazel_rules_go//go/config:static", | 
|  | "//command_line_option:crosstool_top", | 
|  | ], | 
|  | ) | 
|  |  | 
|  | 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: | 
|  | # Skip files-as-fsspecs. | 
|  | if FSSpecInfo not in fsspec: | 
|  | continue | 
|  | fsspecInfo = fsspec[FSSpecInfo] | 
|  | extra_specs.append(fsspecInfo.spec) | 
|  | for f in fsspecInfo.referenced: | 
|  | inputs.append(f) | 
|  |  | 
|  | for file in ctx.files.fsspecs: | 
|  | # Raw .fsspec prototext. No referenced data allowed. | 
|  | extra_specs.append(file) | 
|  |  | 
|  | 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.lz4" | 
|  | 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 (metropolis.node.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("//metropolis/node/build/mkcpio"), | 
|  | executable = True, | 
|  | cfg = "exec", | 
|  | ), | 
|  |  | 
|  | # Allow for transitions to be attached to this rule. | 
|  | "_whitelist_function_transition": attr.label( | 
|  | default = "@bazel_tools//tools/whitelists/function_transition_whitelist", | 
|  | ), | 
|  | }, | 
|  | ) | 
|  |  | 
|  | 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 (metropolis.node.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("//metropolis/node/build/mkerofs"), | 
|  | executable = True, | 
|  | cfg = "host", | 
|  | ), | 
|  |  | 
|  | # Allow for transitions to be attached to this rule. | 
|  | "_whitelist_function_transition": attr.label( | 
|  | default = "@bazel_tools//tools/whitelists/function_transition_whitelist", | 
|  | ), | 
|  | }, | 
|  | ) | 
|  |  | 
|  | # 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", | 
|  | ), | 
|  | "_mkverity": attr.label( | 
|  | doc = "The mkverity executable needed to generate the image.", | 
|  | default = "//metropolis/node/build/mkverity", | 
|  | allow_single_file = True, | 
|  | executable = True, | 
|  | cfg = "host", | 
|  | ), | 
|  | }, | 
|  | ) |