Prevent malloc from being linked into boot_stage2

Prevents Bazel from ever trying to link malloc into the boot_stage2
binary.
diff --git a/bazel/platform/BUILD.bazel b/bazel/platform/BUILD.bazel
index 467c0b1..ca9b829 100644
--- a/bazel/platform/BUILD.bazel
+++ b/bazel/platform/BUILD.bazel
@@ -7,3 +7,9 @@
         "@platforms//cpu:armv6-m",  # This is just FYI.
     ],
 )
+
+# For now, the bootloader is effectively the same platform as the rp2040.
+platform(
+    name = "rp2040_bootloader",
+    parents = [":rp2040"],
+)
diff --git a/bazel/toolchain/objcopy.bzl b/bazel/toolchain/objcopy.bzl
index 153b69b..c1ba06a 100644
--- a/bazel/toolchain/objcopy.bzl
+++ b/bazel/toolchain/objcopy.bzl
@@ -1,5 +1,5 @@
-load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cpp_toolchain", "use_cc_toolchain")
 load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "OBJ_COPY_ACTION_NAME")
+load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cpp_toolchain", "use_cc_toolchain")
 
 def _objcopy_to_bin_impl(ctx):
     cc_toolchain = find_cpp_toolchain(ctx)
diff --git a/bazel/util/BUILD.bazel b/bazel/util/BUILD.bazel
new file mode 100644
index 0000000..ffd0fb0
--- /dev/null
+++ b/bazel/util/BUILD.bazel
@@ -0,0 +1 @@
+package(default_visibility = ["//visibility:public"])
diff --git a/bazel/util/transition.bzl b/bazel/util/transition.bzl
new file mode 100644
index 0000000..ee5947d
--- /dev/null
+++ b/bazel/util/transition.bzl
@@ -0,0 +1,76 @@
+# A transition in Bazel is a way to force changes to the way the build is
+# evaluated for all dependencies of a given rule.
+#
+# Imagine the following simple dependency graph:
+#
+#     ->: depends on
+#     a -> b -> c
+#
+# Normally, if you set `defines` on a, they couldn't apply to b or c because
+# they are dependencies of a. There's no way for b or c to know about a's
+# settings, because they don't even know a exists!
+#
+# We can fix this via a transition! If we put a transition in front of `a`
+# that sets --copts=-DFOO=42, we're telling Bazel to build a and all of its
+# dependencies under that configuration.
+#
+# Note: Flags must be referenced as e.g. `//command_line_option:copt` in
+# transitions.
+#
+# `declare_transition()` eliminates the frustrating amount of boilerplate. All
+# you need to do is provide a set of attrs, and then a `flag_overrides`
+# dictionary that tells `declare_transition()` which attrs to pull flag values
+# from. The common `src` attr tells the transition which build rule to apply
+# the transition to.
+def declare_transtion(attrs, flag_overrides, executable = True):
+    def _flag_override_impl(settings, attrs):
+        return {
+            key: str(getattr(attrs, value))
+            for key, value in flag_overrides.items()
+        }
+
+    _transition = transition(
+        implementation = _flag_override_impl,
+        inputs = [],
+        outputs = flag_overrides.keys(),
+    )
+
+    def _symlink_artifact_impl(ctx):
+        out = ctx.actions.declare_file(ctx.label.name)
+        if executable:
+            ctx.actions.symlink(output = out, target_file = ctx.executable.src)
+            return [DefaultInfo(files = depset([out]), executable = out)]
+
+        ctx.actions.symlink(
+            output = out,
+            target_file = ctx.attr.src[0][DefaultInfo].files.to_list()[0],
+        )
+        return [DefaultInfo(files = depset([out]))]
+
+    return rule(
+        implementation = _symlink_artifact_impl,
+        executable = executable,
+        attrs = {
+            "src": attr.label(
+                cfg = _transition,
+                executable = executable,
+                mandatory = True,
+            ),
+            "_allowlist_function_transition": attr.label(
+                default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+            ),
+        } | attrs,
+    )
+
+rp2040_bootloader_binary = declare_transtion(
+    attrs = {
+        "_malloc": attr.label(default = "//src/rp2_common/boot_stage2:no_malloc"),
+        "_bootloader_platform": attr.label(default = "//bazel/platform:rp2040_bootloader"),
+    },
+    flag_overrides = {
+        "//command_line_option:platforms": "_bootloader_platform",
+        # We don't want --custom_malloc to ever apply to the bootloader, so
+        # always explicitly override it here.
+        "//command_line_option:custom_malloc": "_malloc",
+    },
+)
diff --git a/src/rp2_common/boot_stage2/BUILD.bazel b/src/rp2_common/boot_stage2/BUILD.bazel
index e6c175d..df6a041 100644
--- a/src/rp2_common/boot_stage2/BUILD.bazel
+++ b/src/rp2_common/boot_stage2/BUILD.bazel
@@ -1,6 +1,7 @@
 load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
 load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
 load("//bazel/toolchain:objcopy.bzl", "objcopy_to_bin")
+load("//bazel/util:transition.bzl", "rp2040_bootloader_binary")
 
 package(default_visibility = ["//visibility:private"])
 
@@ -32,7 +33,7 @@
 )
 
 cc_binary(
-    name = "boot_stage2_elf",
+    name = "boot_stage2_elf_actual",
     srcs = ["compile_time_choice.S"],
     copts = ["-fPIC"],
     linkopts = [
@@ -40,7 +41,10 @@
         "-nostartfiles",
         "-T$(location boot_stage2.ld)",
     ],
+    # this does nothing if someone passes --custom_malloc, so the
+    # rp2040_bootloader_binary transition forcibly clobbers --custom_malloc.
     malloc = ":no_malloc",
+    tags = ["manual"],
     deps = [
         "boot_stage2.ld",
         ":config",
@@ -49,6 +53,12 @@
     ],
 )
 
+# Always build the bootloader with the bootloader-specific platform.
+rp2040_bootloader_binary(
+    name = "boot_stage2_elf",
+    src = "boot_stage2_elf_actual",
+)
+
 objcopy_to_bin(
     name = "boot_stage2_bin",
     src = ":boot_stage2_elf",