b/t/llvm-efi: add EFI toolchain based on LLVM

This adds a Bazel toolchain for building EFI binaries using rules_cc
with LLVM installed in the container.

It does not yet add an EFI standard library.

Change-Id: I9eb15de6f4f800ab6351607d2fb01dad3135da9f
Reviewed-on: https://review.monogon.dev/c/monogon/+/333
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/build/toolchain/llvm-efi/BUILD b/build/toolchain/llvm-efi/BUILD
new file mode 100644
index 0000000..3cfa67f
--- /dev/null
+++ b/build/toolchain/llvm-efi/BUILD
@@ -0,0 +1,32 @@
+load(":cc_toolchain_config.bzl", "efi_k8_cc_toolchain_config")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(name = "empty")
+
+cc_toolchain_suite(
+    name = "efi_cc_suite",
+    toolchains = {
+        "k8": ":efi_k8_cc_toolchain",
+    },
+)
+
+filegroup(
+    name = "fltused",
+    srcs = ["fltused.o"],
+)
+
+cc_toolchain(
+    name = "efi_k8_cc_toolchain",
+    all_files = ":empty",
+    compiler_files = ":empty",
+    dwp_files = ":empty",
+    linker_files = ":fltused",
+    objcopy_files = ":empty",
+    strip_files = ":empty",
+    supports_param_files = 0,
+    toolchain_config = ":efi_k8_cc_toolchain_config",
+    toolchain_identifier = "efi-k8-toolchain",
+)
+
+efi_k8_cc_toolchain_config(name = "efi_k8_cc_toolchain_config")
diff --git a/build/toolchain/llvm-efi/README.md b/build/toolchain/llvm-efi/README.md
new file mode 100644
index 0000000..522fcfd
--- /dev/null
+++ b/build/toolchain/llvm-efi/README.md
@@ -0,0 +1,28 @@
+llvm-efi
+========
+
+llvm-efi is a Bazel cc toolchain that uses the machine's host LLVM/clang with flags targeting freestanding EFI.
+EFI headers are not shipped as part of the toolchain, but are available as a cc_library from `@gnuefi//:gnuefi`.
+
+At some point, this toolchain should be replaced by a fully hermetic toolchain that doesn't depend on the host environment.
+
+Usage
+-----
+
+To use this toolchain explicitely while building a `cc_binary`, do:
+
+    bazel build --crosstool_top=//build/toolchain/llvm-efi:efi_cc_suite //foo/bar
+
+During an actual build however, the right toolchain should be selected using aspects or other Bazel configurability features, instead of a hardcoded `--crosstool_top`.
+
+fltused
+-------
+
+This is a special symbol emitted by MSVC-compatible compilers. In an EFI environment it can be ignored, but it needs to
+be defined. See fltused.c for more information on the symbol. Since we cannot build an object file with Bazel and
+building things for toolchains isn't a thing anyways, this file is prebuilt. If this ever needs to be rebuilt (which
+will probably never happen since there is only one static symbol in there) this can be done with the following clang
+invocation:
+
+    clang -target x86_64-unknown-windows -fno-ms-compatibility -fno-ms-extensions -ffreestanding -o fltused.o .o -c fltused.c
+   
diff --git a/build/toolchain/llvm-efi/cc_toolchain_config.bzl b/build/toolchain/llvm-efi/cc_toolchain_config.bzl
new file mode 100644
index 0000000..697b675
--- /dev/null
+++ b/build/toolchain/llvm-efi/cc_toolchain_config.bzl
@@ -0,0 +1,217 @@
+load("@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl", "feature", "flag_group", "flag_set", "tool", "tool_path", "with_feature_set")
+load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
+
+all_compile_actions = [
+    ACTION_NAMES.c_compile,
+    ACTION_NAMES.cpp_compile,
+    ACTION_NAMES.linkstamp_compile,
+    ACTION_NAMES.assemble,
+    ACTION_NAMES.preprocess_assemble,
+    ACTION_NAMES.cpp_header_parsing,
+    ACTION_NAMES.cpp_module_compile,
+    ACTION_NAMES.cpp_module_codegen,
+    ACTION_NAMES.clif_match,
+    ACTION_NAMES.lto_backend,
+]
+
+all_cpp_compile_actions = [
+    ACTION_NAMES.cpp_compile,
+    ACTION_NAMES.linkstamp_compile,
+    ACTION_NAMES.cpp_header_parsing,
+    ACTION_NAMES.cpp_module_compile,
+    ACTION_NAMES.cpp_module_codegen,
+    ACTION_NAMES.clif_match,
+]
+
+preprocessor_compile_actions = [
+    ACTION_NAMES.c_compile,
+    ACTION_NAMES.cpp_compile,
+    ACTION_NAMES.linkstamp_compile,
+    ACTION_NAMES.preprocess_assemble,
+    ACTION_NAMES.cpp_header_parsing,
+    ACTION_NAMES.cpp_module_compile,
+    ACTION_NAMES.clif_match,
+]
+
+codegen_compile_actions = [
+    ACTION_NAMES.c_compile,
+    ACTION_NAMES.cpp_compile,
+    ACTION_NAMES.linkstamp_compile,
+    ACTION_NAMES.assemble,
+    ACTION_NAMES.preprocess_assemble,
+    ACTION_NAMES.cpp_module_codegen,
+    ACTION_NAMES.lto_backend,
+]
+
+all_link_actions = [
+    ACTION_NAMES.cpp_link_executable,
+    ACTION_NAMES.cpp_link_dynamic_library,
+    ACTION_NAMES.cpp_link_nodeps_dynamic_library,
+]
+
+lto_index_actions = [
+    ACTION_NAMES.lto_index_for_executable,
+    ACTION_NAMES.lto_index_for_dynamic_library,
+    ACTION_NAMES.lto_index_for_nodeps_dynamic_library,
+]
+
+# This defines a relatively minimal EFI toolchain based on host LLVM and no standard library or headers.
+def _efi_k8_cc_toolchain_impl(ctx):
+    default_compile_flags_feature = feature(
+        name = "default_compile_flags",
+        enabled = True,
+        flag_sets = [
+            flag_set(
+                actions = all_compile_actions,
+                flag_groups = ([
+                    flag_group(
+                        flags = ["-target", "x86_64-unknown-windows"],
+                    ),
+                ]),
+            ),
+            flag_set(
+                actions = all_compile_actions,
+                flag_groups = ([
+                    flag_group(
+                        flags = ["-g"],
+                    ),
+                ]),
+                with_features = [with_feature_set(features = ["dbg"])],
+            ),
+            flag_set(
+                actions = all_compile_actions,
+                flag_groups = ([
+                    flag_group(
+                        # Don't bother with O3, this is an EFI toolchain. It's unlikely to gain much performance here
+                        # and increases the risk of dangerous optimizations.
+                        flags = ["-O2", "-DNDEBUG"],
+                    ),
+                ]),
+                with_features = [with_feature_set(features = ["opt"])],
+            ),
+        ],
+    )
+
+    # "Hybrid" mode disables some MSVC C extensions (but keeps its ABI), but still identifies as MSVC.
+    # This is useful if code requires GNU extensions to work which are silently ignored in full MSVC mode.
+    # As EFI does not include Windows headers which depend on nonstandard C behavior this should be fine for most code.
+    # If this feature is disabled, the toolchain runs with MSVC C extensions fully enabled.
+    hybrid_gnu_msvc_feature = feature(
+        name = "hybrid_gnu_msvc",
+        enabled = True,
+        flag_sets = [
+            flag_set(
+                actions = all_compile_actions,
+                flag_groups = [
+                    flag_group(
+                        flags = ["-D_MSC_VER=1920", "-fno-ms-compatibility", "-fno-ms-extensions"],
+                    ),
+                ],
+            ),
+        ],
+    )
+
+    default_link_flags_feature = feature(
+        name = "default_link_flags",
+        enabled = True,
+        flag_sets = [
+            flag_set(
+                actions = all_link_actions + lto_index_actions,
+                flag_groups = ([
+                    flag_group(
+                        flags = [
+                            "-target",
+                            "x86_64-unknown-windows",
+                            "-fuse-ld=lld",
+                            "-Wl,-entry:efi_main",
+                            "-Wl,-subsystem:efi_application",
+                            "-Wl,/BASE:0x0",
+                            "-nostdlib",
+                            "build/toolchain/llvm-efi/fltused.o",
+                        ],
+                    ),
+                ]),
+            ),
+        ],
+    )
+
+    lto_feature = feature(
+        name = "lto",
+        enabled = False,
+        flag_sets = [
+            flag_set(
+                actions = all_compile_actions + all_link_actions,
+                flag_groups = ([
+                    flag_group(
+                        flags = [
+                            "-flto",
+                        ],
+                    ),
+                ]),
+            ),
+        ],
+    )
+
+    tool_paths = [
+        tool_path(
+            name = "gcc",
+            path = "/usr/bin/clang",
+        ),
+        tool_path(
+            name = "ld",
+            path = "/usr/bin/lld-link",
+        ),
+        tool_path(
+            name = "ar",
+            path = "/usr/bin/llvm-ar",
+        ),
+        tool_path(
+            name = "cpp",
+            path = "/bin/false",
+        ),
+        tool_path(
+            name = "gcov",
+            path = "/bin/false",
+        ),
+        tool_path(
+            name = "nm",
+            path = "/usr/bin/llvm-nm",
+        ),
+        tool_path(
+            name = "objcopy",
+            # We can't use LLVM's objcopy until we pick up https://reviews.llvm.org/D106942
+            path = "/usr/bin/objcopy",
+        ),
+        tool_path(
+            name = "objdump",
+            path = "/usr/bin/llvm-objdump",
+        ),
+        tool_path(
+            name = "strip",
+            path = "/usr/bin/llvm-strip",
+        ),
+    ]
+
+    return cc_common.create_cc_toolchain_config_info(
+        ctx = ctx,
+        features = [default_link_flags_feature, default_compile_flags_feature, hybrid_gnu_msvc_feature, lto_feature],
+        # Needed for various compiler built-in headers and auxiliary data. No system libraries are being used.
+        cxx_builtin_include_directories = [
+            "/usr/lib64/clang",
+        ],
+        toolchain_identifier = "k8-toolchain",
+        host_system_name = "local",
+        target_system_name = "x86_64-efi",
+        target_cpu = "k8",
+        target_libc = "none",
+        compiler = "clang",
+        abi_version = "none",
+        abi_libc_version = "none",
+        tool_paths = tool_paths,
+    )
+
+efi_k8_cc_toolchain_config = rule(
+    implementation = _efi_k8_cc_toolchain_impl,
+    attrs = {},
+    provides = [CcToolchainConfigInfo],
+)
diff --git a/build/toolchain/llvm-efi/fltused.c b/build/toolchain/llvm-efi/fltused.c
new file mode 100644
index 0000000..a6ca646
--- /dev/null
+++ b/build/toolchain/llvm-efi/fltused.c
@@ -0,0 +1,6 @@
+// This is a marker symbol emitted by MSVC-ABI compatible compilers. Its presence indicates that the linked binary
+// contains instructions working with floating-point registers. Since we do not have a standard library which consumes
+// it we can just define it as zero.
+// See https://github.com/rust-lang/rust/issues/62785#issuecomment-531186089 for more discussion.
+// Since building static libraries is not possible with Bazel this is compiled and checked in.
+int _fltused __attribute__((used)) = 0;
\ No newline at end of file
diff --git a/build/toolchain/llvm-efi/fltused.o b/build/toolchain/llvm-efi/fltused.o
new file mode 100644
index 0000000..bb55d4c
--- /dev/null
+++ b/build/toolchain/llvm-efi/fltused.o
Binary files differ
diff --git a/build/toolchain/llvm-efi/transition.bzl b/build/toolchain/llvm-efi/transition.bzl
new file mode 100644
index 0000000..00c1433
--- /dev/null
+++ b/build/toolchain/llvm-efi/transition.bzl
@@ -0,0 +1,15 @@
+def _build_efi_transition_impl(settings, attr):
+    """
+    Transition that enables building for an EFI environment. Currently ony supports C code.
+    """
+    return {
+        "//command_line_option:crosstool_top": "//build/toolchain/llvm-efi:efi_cc_suite",
+    }
+
+build_efi_transition = transition(
+    implementation = _build_efi_transition_impl,
+    inputs = [],
+    outputs = [
+        "//command_line_option:crosstool_top",
+    ],
+)