[bazel] Add bazel aspects for dumping toolchain side-products

Signed-off-by: Miguel Young de la Sota <mcyoung@google.com>
diff --git a/rules/cc_side_outputs.bzl b/rules/cc_side_outputs.bzl
new file mode 100644
index 0000000..e363fdb
--- /dev/null
+++ b/rules/cc_side_outputs.bzl
@@ -0,0 +1,281 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+"""Aspects and rules for making cc_* libraries emit more outputs."""
+
+load("@rules_cc//cc:action_names.bzl", "ACTION_NAMES", "C_COMPILE_ACTION_NAME")
+load("//rules:bugfix.bzl", "find_cc_toolchain")
+load("//rules:rv.bzl", "rv_rule")
+
+CcSideProductInfo = provider(fields = ["files"])
+
+def _cc_compile_different_output(target, ctx, extension, flags):
+    """
+    Helper macro for implementing the .s and .ll outputting libraries.
+
+    In addition to the usual target and ctx inputs for an aspect, it also
+    takes an extension to add to each source file of the rule being analyzed,
+    and flags to add to the compiler arguments to get the output we want.
+    """
+    if ctx.rule.kind not in ["cc_library", "cc_binary", "cc_test"]:
+        return [CcSideProductInfo(files = depset([]))]
+
+    # This filters out both headers and assembly source inputs, neither of which
+    # make sense to generate an .s output for.
+    translation_units = [
+        src.files.to_list()[0]
+        for src in ctx.rule.attr.srcs
+        if src.files.to_list()[0].extension in [
+            # We only use .cc, but to avoid nasty surprises we support all five
+            # C++ file extensions.
+            "c", "cc", "cpp", "cxx", "c++", "C",
+        ]
+    ]
+
+    transitive = []
+    for dep in ctx.rule.attr.deps:
+        if CcSideProductInfo in dep:
+            transitive.append(dep[CcSideProductInfo].files)
+
+    # Libraries consisting of just headers or assembly files have nothing
+    # useful to contribute to the output.
+    if len(translation_units) == 0:
+        return [CcSideProductInfo(files = depset(
+            transitive = transitive,
+        ))]
+
+    if CcInfo in target:
+        cc_info = target[CcInfo]
+    else:
+        # Some rules, like cc_binary, do NOT produce a CcInfo provider. Therefore,
+        # we need to build one from its dependencies.
+        cc_info = cc_common.merge_cc_infos(
+            direct_cc_infos = [dep[CcInfo] for dep in ctx.rule.attr.deps if CcInfo in dep],
+        )
+    cc_compile_ctx = cc_info.compilation_context
+
+    cc_toolchain = find_cc_toolchain(ctx)
+    feature_configuration = cc_common.configure_features(
+        ctx = ctx,
+        cc_toolchain = cc_toolchain,
+        requested_features = ctx.features,
+        unsupported_features = ctx.disabled_features,
+    )
+
+    c_compiler_path = cc_common.get_tool_for_action(
+        feature_configuration = feature_configuration,
+        action_name = ACTION_NAMES.c_compile,
+    )
+
+    outputs = []
+    for source_file in translation_units:
+        output_file = ctx.actions.declare_file(
+            # Some source files in the repo are currently pulled in by multiple
+            # rules, and this is allowed in general, although not a good idea.
+            # 
+            # Adding the rule name in front of the file name helps mitigate this
+            # issue.
+            "{}.{}.{}".format(target.label.name, source_file.basename, extension),
+        )
+        outputs.append(output_file)
+
+        # C files are treated specially, and have different flags applied
+        # (e.g. --std=c11).
+        opts = ctx.fragments.cpp.copts + ctx.rule.attr.copts
+        if source_file.extension == "c":
+            opts += ctx.fragments.cpp.conlyopts + ctx.rule.attr.conlyopts
+        else:
+            opts += ctx.fragments.cpp.cxxopts + ctx.rule.attr.cxxopts
+
+        c_compile_variables = cc_common.create_compile_variables(
+            feature_configuration = feature_configuration,
+            cc_toolchain = cc_toolchain,
+            source_file = source_file.path,
+            output_file = output_file.path,
+            user_compile_flags = opts + flags,
+            include_directories = depset(
+                direct = [src.dirname for src in cc_compile_ctx.headers.to_list()],
+                transitive = [cc_compile_ctx.includes],
+            ),
+            quote_include_directories = cc_compile_ctx.quote_includes,
+            system_include_directories = cc_compile_ctx.system_includes,
+            framework_include_directories = cc_compile_ctx.framework_includes,
+            preprocessor_defines = depset(
+                direct = ctx.rule.attr.local_defines,
+                transitive = [cc_compile_ctx.defines],
+            ),
+        )
+
+        command_line = cc_common.get_memory_inefficient_command_line(
+            feature_configuration = feature_configuration,
+            action_name = ACTION_NAMES.c_compile,
+            variables = c_compile_variables,
+        )
+        env = cc_common.get_environment_variables(
+            feature_configuration = feature_configuration,
+            action_name = ACTION_NAMES.c_compile,
+            variables = c_compile_variables,
+        )
+
+        ctx.actions.run(
+            executable = c_compiler_path,
+            arguments = command_line,
+            env = env,
+            inputs = depset(
+                [source_file],
+                transitive = [
+                    cc_toolchain.all_files,
+                    cc_compile_ctx.headers,
+                ],
+            ),
+            outputs = [output_file],
+        )
+
+    return [CcSideProductInfo(files = depset(
+        direct = outputs,
+        transitive = transitive,
+    ))]
+
+def _cc_assembly_aspect_impl(target, ctx):
+    return _cc_compile_different_output(target, ctx, "s", ["-S"])
+
+cc_asm_aspect = aspect(
+    implementation = _cc_assembly_aspect_impl,
+    doc = """
+        An aspect that provides a CcSideProductInfo containing the assembly file outputs
+        of every C/C++ translation unit in the sources of the rule it is applied to and
+        all of its dependencies.
+    """,
+    attrs = {
+        "_cc_toolchain": attr.label(
+            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
+        ),
+    },
+    attr_aspects = ["deps"],
+    provides = [CcSideProductInfo],
+    toolchains = ["@rules_cc//cc:toolchain_type"],
+    incompatible_use_toolchain_transition = True,
+    fragments = ["cpp"],
+    host_fragments = ["cpp"],
+)
+
+def _cc_llvm_aspect_impl(target, ctx):
+    cc_toolchain = find_cc_toolchain(ctx)
+    if cc_toolchain.compiler.find("clang") == -1:
+        return CcSideProductInfo(files = depset())
+    return _cc_compile_different_output(target, ctx, "ll", ["-S", "-emit-llvm"])
+
+cc_llvm_aspect = aspect(
+    implementation = _cc_llvm_aspect_impl,
+    doc = """
+        An aspect that provides a CcSideProductInfo containing the LLVM IR file outputs
+        of every C/C++ translation unit in the sources of the rule it is applied to and
+        all of its dependencies.
+
+        If the current compiler does not appear to be clang, it outputs nothing instead.
+    """,
+    attrs = {
+        "_cc_toolchain": attr.label(
+            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
+        ),
+    },
+    attr_aspects = ["deps"],
+    provides = [CcSideProductInfo],
+    toolchains = ["@rules_cc//cc:toolchain_type"],
+    incompatible_use_toolchain_transition = True,
+    fragments = ["cpp"],
+    host_fragments = ["cpp"],
+)
+
+MapFile = provider(fields = ["map_file"])
+
+def _cc_relink_with_linkmap_aspect_impl(target, ctx):
+    # As mentioned above, there is no CcInfo in a cc_binary, so we're forced
+    # to rumage around the target's actions for its link action, and then
+    # re-run it, but capturing the map file as a side-product.
+    link_action = None
+    for action in target.actions:
+        if action.mnemonic == "CppLink":
+            link_action = action
+            break
+    if not link_action:
+        return [MapFile(map_file = None)]
+
+    output_file = ctx.actions.declare_file(target.label.name + ".ldmap")
+
+    cc_toolchain = find_cc_toolchain(ctx)
+    feature_configuration = cc_common.configure_features(
+        ctx = ctx,
+        cc_toolchain = cc_toolchain,
+        requested_features = ctx.features,
+        unsupported_features = ctx.disabled_features,
+    )
+    linker_path = cc_common.get_tool_for_action(
+        feature_configuration = feature_configuration,
+        action_name = ACTION_NAMES.cpp_link_executable,
+    )
+
+    ctx.actions.run(
+        executable = linker_path,
+        arguments = link_action.argv[1:] + ["-Wl,-Map=" + output_file.path],
+        env = link_action.env,
+        inputs = link_action.inputs,
+        outputs = [output_file],
+    )
+
+    return [MapFile(map_file = output_file)]
+
+cc_relink_with_linkmap_aspect = aspect(
+    implementation = _cc_relink_with_linkmap_aspect_impl,
+    doc = """
+        An aspect to apply to a cc_binary rule that rebuilds its linker invocation and
+        re-runs it, capturing the map file as an output.
+
+        This needs to exist because cc_binary currently does not allow us to specify that
+        it may emit multiple outputs.
+    """,
+    attrs = {
+        "_cc_toolchain": attr.label(
+            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
+        ),
+    },
+    provides = [MapFile],
+    toolchains = ["@rules_cc//cc:toolchain_type"],
+    incompatible_use_toolchain_transition = True,
+    fragments = ["cpp"],
+    host_fragments = ["cpp"],
+)
+
+def _rv_asm_impl(ctx):
+    return [DefaultInfo(
+        files = ctx.attr.target[CcSideProductInfo].files,
+        data_runfiles = ctx.runfiles(transitive_files = ctx.attr.target[CcSideProductInfo].files),
+    )]
+
+rv_asm = rv_rule(
+    implementation = _rv_asm_impl,
+    attrs = {"target": attr.label(aspects = [cc_asm_aspect])},
+)
+
+def _llvm_ir_impl(ctx):
+    return [DefaultInfo(
+        files = ctx.attr.target[CcSideProductInfo].files,
+        data_runfiles = ctx.runfiles(transitive_files = ctx.attr.target[CcSideProductInfo].files),
+    )]
+
+rv_llvm_ir = rv_rule(
+    implementation = _rv_asm_impl,
+    attrs = {"target": attr.label(aspects = [cc_llvm_aspect])},
+)
+
+def _cc_relink_with_linkmap_impl(ctx):
+    return [DefaultInfo(
+        files = depset([ctx.attr.target[MapFile].map_file]),
+        data_runfiles = ctx.runfiles(files = [ctx.attr.target[MapFile].map_file]),
+    )]
+
+rv_relink_with_linkmap = rv_rule(
+    implementation = _cc_relink_with_linkmap_impl,
+    attrs = {"target": attr.label(aspects = [cc_relink_with_linkmap_aspect])},
+)