diff --git a/utils/bazel/llvm-project-overlay/llvm/BUILD.bazel b/utils/bazel/llvm-project-overlay/llvm/BUILD.bazel
--- a/utils/bazel/llvm-project-overlay/llvm/BUILD.bazel
+++ b/utils/bazel/llvm-project-overlay/llvm/BUILD.bazel
@@ -57,7 +57,6 @@
         out = "include/llvm/Config/{}.def".format(path),
         macro_name = macro_name,
         valid_targets = targets_with_file(pattern),
-        targets = ":targets",
     ) for name, macro_name, path, pattern in [
         ("targets_def_gen", "TARGET", "Targets", "CMakeLists.txt"),
         ("asm_printers_def_gen", "ASM_PRINTER", "AsmPrinters", "*AsmPrinter.cpp"),
diff --git a/utils/bazel/llvm-project-overlay/llvm/enum_targets_gen.bzl b/utils/bazel/llvm-project-overlay/llvm/enum_targets_gen.bzl
--- a/utils/bazel/llvm-project-overlay/llvm/enum_targets_gen.bzl
+++ b/utils/bazel/llvm-project-overlay/llvm/enum_targets_gen.bzl
@@ -22,7 +22,6 @@
     out = "include/llvm/Config/Disassemblers.def",
     macro_name = "DISASSEMBLER",
     valid_targets = ["AArch64", "X86"],
-    targets = ":targets",
 )
 
 This rule provides a slightly more semantic API than template_rule, but the main
@@ -30,47 +29,32 @@
 a select is not allowed to be passed to a rule within another data structure. 
 """
 
-load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
-
-def _enum_targets_gen_impl(ctx):
-    to_replace = "@LLVM_ENUM_{}S@".format(ctx.attr.macro_name)
+load(":expand_template.bzl", "expand_template")
+load(":target_macros.bzl", "is_enabled")
+
+def enum_targets_gen(name, src, out, macro_name, valid_targets):
+    """Replaces @LLVM_ENUM_FOOS@ with a list of macro invocations.
+
+    Args:
+      name: The name of the generated BUILD target.
+      src: The input file to expand.
+      out: The output file to generate.
+      macro_name: The name of the enumeration. This is the suffix of the
+          placeholder being replaced `@LLVM_ENUM_{}S@` and of the
+          macro invocations generated `LLVM_{}(TARGET)`. Should be
+          all caps and singular, e.g. 'DISASSEMBLER'.
+      valid_targets: A list of which LLVM targets are valid for expansion.
+          Only enabled LLVM targets are actually expanded.
+    """
+    to_replace = "@LLVM_ENUM_{}S@".format(macro_name)
     replacement = "\n".join([
-        "LLVM_{}({})".format(ctx.attr.macro_name, t)
-        for t in ctx.attr.targets[BuildSettingInfo].value
-        if t in ctx.attr.valid_targets
+        "LLVM_{}({})".format(macro_name, t)
+        for t in valid_targets
+        if is_enabled(t)
     ])
-    ctx.actions.expand_template(
-        template = ctx.file.src,
-        output = ctx.outputs.out,
+    expand_template(
+        name = name,
+        src = src,
+        out = out,
         substitutions = {to_replace: replacement},
     )
-
-enum_targets_gen = rule(
-    implementation = _enum_targets_gen_impl,
-    attrs = {
-        "src": attr.label(
-            mandatory = True,
-            allow_single_file = True,
-        ),
-        "valid_targets": attr.string_list(
-            mandatory = True,
-            doc = "A static list of LLVM targets that are valid for this enum. " +
-                  "Since this value depends on what files are present, it " +
-                  "must be evaluated outside the implementation of the rule.",
-        ),
-        "targets": attr.label(
-            mandatory = True,
-            doc = "A configuration of LLVM targets to build.",
-        ),
-        "out": attr.output(mandatory = True),
-        "macro_name": attr.string(
-            mandatory = True,
-            doc = "The name of the enumeration. This is the suffix of the" +
-                  " placeholder being replaced `@LLVM_ENUM_{}S@` and of the" +
-                  " macro invocations generated `LLVM_{}(TARGET)`. Should be" +
-                  " all caps and singular, e.g. 'DISASSEMBLER'",
-        ),
-    },
-    # output_to_genfiles is required for header files.
-    output_to_genfiles = True,
-)
diff --git a/utils/bazel/llvm-project-overlay/llvm/expand_template.bzl b/utils/bazel/llvm-project-overlay/llvm/expand_template.bzl
new file mode 100644
--- /dev/null
+++ b/utils/bazel/llvm-project-overlay/llvm/expand_template.bzl
@@ -0,0 +1,30 @@
+# This file is licensed under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+"""A rule to expand templates with substitutions.
+
+This is a wrapper around `actions.expand_template` so that we can use it
+from within macros.
+"""
+
+def _expand_template_impl(ctx):
+    ctx.actions.expand_template(
+        template = ctx.file.src,
+        output = ctx.outputs.out,
+        substitutions = ctx.attr.substitutions,
+    )
+    return DefaultInfo(files = depset([ctx.outputs.out]))
+
+expand_template = rule(
+    implementation = _expand_template_impl,
+    attrs = {
+        "src": attr.label(
+            mandatory = True,
+            allow_single_file = True,
+        ),
+        "out": attr.output(mandatory = True),
+        "substitutions": attr.string_dict(),
+    },
+    output_to_genfiles = True,
+)
diff --git a/utils/bazel/llvm-project-overlay/llvm/target_macros.bzl b/utils/bazel/llvm-project-overlay/llvm/target_macros.bzl
--- a/utils/bazel/llvm-project-overlay/llvm/target_macros.bzl
+++ b/utils/bazel/llvm-project-overlay/llvm/target_macros.bzl
@@ -16,6 +16,12 @@
     matches = native.glob(["lib/Target/*/{}".format(suffix)])
     return [m.split("/")[2] for m in matches]
 
+def is_enabled(target):
+    return select({
+          ":{}_enabled".format(target): True,
+          "//conditions:default": False,
+        })
+
 def enabled_targets(pattern):
     """Build a list of enabled LLVM targets.
 
@@ -26,7 +32,7 @@
     # depending on the `:targets` flag.
     selects = [
         select({
-            ":%s_enabled" % t: ["{}{}".format(t, pattern)],
+            ":{}_enabled".format(t): ["{}{}".format(t, pattern)],
             "//conditions:default": [],
         })
         for t in llvm_targets