[bazel][idk] Add idk_rustc_binary_host_tool()

The macro wraps to rustc_binary_host_tool() rather than having it passed
as the `tool` parameter. This follows the pattern of
idk_cc_binary_host_tool().

Also introduce the GN template sdk_rustc_binary_host_tool(), which
bazel2gn generates for uses of the new macro in Bazel files.

Update fake-omaha-client to use the revised macro.

Remove idk_host_tool() now that all such tools use the wrapping macros.

Bug: 442025401
Test: fx bazel build --config=host //src/sys/pkg/testing/fake-omaha-client
Test: fx build //build/bazel/rules/tests //sdk:final_fuchsia_idk.validation
Change-Id: Ib39a9e0c1ee5b0c21145c5f2686af4b153ef9f21
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/1427304
Reviewed-by: David Turner <digit@google.com>
Commit-Queue: David Dorwin <ddorwin@google.com>
diff --git a/build/bazel/bazel_idk/defs.bzl b/build/bazel/bazel_idk/defs.bzl
index 0e4ccb6..f2eb700 100644
--- a/build/bazel/bazel_idk/defs.bzl
+++ b/build/bazel/bazel_idk/defs.bzl
@@ -28,7 +28,7 @@
     "//build/bazel/bazel_idk/private:idk_host_tool.bzl",
     _idk_cc_binary_host_tool = "idk_cc_binary_host_tool",
     _idk_go_binary_host_tool = "idk_go_binary_host_tool",
-    _idk_host_tool = "idk_host_tool",
+    _idk_rustc_binary_host_tool = "idk_rustc_binary_host_tool",
 )
 load(
     "//build/bazel/bazel_idk/private:idk_molecule.bzl",
@@ -52,4 +52,4 @@
 
 idk_cc_binary_host_tool = _idk_cc_binary_host_tool
 idk_go_binary_host_tool = _idk_go_binary_host_tool
-idk_host_tool = _idk_host_tool
+idk_rustc_binary_host_tool = _idk_rustc_binary_host_tool
diff --git a/build/bazel/bazel_idk/private/idk_host_tool.bzl b/build/bazel/bazel_idk/private/idk_host_tool.bzl
index c2d700d..669e400 100644
--- a/build/bazel/bazel_idk/private/idk_host_tool.bzl
+++ b/build/bazel/bazel_idk/private/idk_host_tool.bzl
@@ -5,7 +5,7 @@
 """Rule for defining IDK host tools."""
 
 load("@platforms//host:constraints.bzl", "HOST_CONSTRAINTS")
-load("//build/bazel/rules/host:defs.bzl", "cc_binary_host_tool", "go_binary_host_tool")
+load("//build/bazel/rules/host:defs.bzl", "cc_binary_host_tool", "go_binary_host_tool", "rustc_binary_host_tool")
 load(":idk_atom.bzl", "idk_atom")
 load(
     ":idk_common.bzl",
@@ -118,27 +118,29 @@
     },
 )
 
-# A wrapper that appends "_idk" to the name. This avoids duplicate name errors
-# that could occur if using the symbolic macro above directly.
-# TODO(https://fxbug.dev/442025401): Consider removing this or the
-# language-specific versions after migrating all tools to macros like
-# `cc_binary_host_tool()`.
-def idk_host_tool(name, category, tool, **kwargs):
-    """Defines a host tool in the IDK.
-
-    GN note: Unlike the GN template, `name` should not include "_sdk"/"_idk".
-
-    Args:
-        name: The name of the tool binary.
-        tool: A list containing a single label of the tool binary.
-        **kwargs: See `_idk_host_tool()` for details.
-    """
-    _idk_host_tool(
-        name = name + "_idk",
-        category = category,
-        tool = tool[0],
-        **kwargs
-    )
+_BINARY_HOST_TOOL_ATTRS = {
+    "idk_name": attr.string(
+        doc = "The name of the tool in the IDK. Usually matches `name`.",
+        mandatory = True,
+        configurable = False,
+    ),
+    "category": attr.string(
+        doc = "Publication level of the tool in the IDK. See _create_idk_atom().",
+        mandatory = True,
+        configurable = False,
+    ),
+    "api_area": attr.string(
+        doc = "The API area responsible for maintaining this tool.",
+        mandatory = True,
+    ),
+    # TODO(https://fxbug.dev/460538634): Remove once bazel2gn is no longer
+    # being used for host tools.
+    "target_compatible_with": attr.string_list(
+        doc = "Standard meaning. Must be `HOST_CONSTRAINTS`.",
+        default = HOST_CONSTRAINTS,
+        configurable = False,
+    ),
+}
 
 def _idk_cc_binary_host_tool_impl(
         name,
@@ -174,29 +176,7 @@
     """,
     implementation = _idk_cc_binary_host_tool_impl,
     inherit_attrs = cc_binary_host_tool,
-    attrs = {
-        "idk_name": attr.string(
-            doc = "The name of the tool in the IDK. Usually matches `name`.",
-            mandatory = True,
-            configurable = False,
-        ),
-        "category": attr.string(
-            doc = "Publication level of the tool in the IDK. See _create_idk_atom().",
-            mandatory = True,
-            configurable = False,
-        ),
-        "api_area": attr.string(
-            doc = "The API area responsible for maintaining this tool.",
-            mandatory = True,
-        ),
-        # TODO(https://fxbug.dev/460538634): Remove once bazel2gn is no longer
-        # being used for host tools.
-        "target_compatible_with": attr.string_list(
-            doc = "Standard meaning. Must be `HOST_CONSTRAINTS`.",
-            default = HOST_CONSTRAINTS,
-            configurable = False,
-        ),
-    },
+    attrs = _BINARY_HOST_TOOL_ATTRS,
 )
 
 # This must be a legacy macro with `**kwargs` because go_binary_host_tool is a
@@ -241,3 +221,40 @@
         tool = binary_name,
         target_compatible_with = HOST_CONSTRAINTS,
     )
+
+def _idk_rustc_binary_host_tool_impl(
+        name,
+        idk_name,
+        category,
+        api_area,
+        target_compatible_with,
+        **kwargs):
+    if target_compatible_with != HOST_CONSTRAINTS:
+        fail("`target_compatible_with` must be `%s`." % HOST_CONSTRAINTS)
+
+    binary_name = name
+
+    rustc_binary_host_tool(
+        name = binary_name,
+        target_compatible_with = HOST_CONSTRAINTS,
+        **kwargs
+    )
+
+    _idk_host_tool(
+        name = name + "_idk",
+        idk_name = idk_name,
+        category = category,
+        api_area = api_area,
+        tool = binary_name,
+        target_compatible_with = HOST_CONSTRAINTS,
+    )
+
+idk_rustc_binary_host_tool = macro(
+    doc = """Defines a `rustc_binary()` host tool in the IDK.
+
+    GN note: Unlike some GN templates, `name` should not include "_sdk"/"_idk".
+    """,
+    implementation = _idk_rustc_binary_host_tool_impl,
+    inherit_attrs = rustc_binary_host_tool,
+    attrs = _BINARY_HOST_TOOL_ATTRS,
+)
diff --git a/build/sdk/sdk_host_tool.gni b/build/sdk/sdk_host_tool.gni
index c6b2d42..a191014a 100644
--- a/build/sdk/sdk_host_tool.gni
+++ b/build/sdk/sdk_host_tool.gni
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//build/go/go_binary.gni")
+import("//build/rust/rustc_binary.gni")
 import("//build/sdk/sdk_atom.gni")
 
 _idk_only_parameters = [
@@ -182,3 +183,38 @@
     deps = [ ":${tool_name}" ]
   }
 }
+
+# Defines a Rust binary host tool and its IDK atom.
+#
+# Parameters
+#   category (required)
+#     Publication level of the executable in the IDK.
+#     See //build/sdk/sdk_atom.gni.
+#
+#   sdk_area (optional)
+#     [string] The API area responsible for maintaining this host tool.
+#     See //build/sdk/sdk_atom.gni.
+#
+#   sdk_name (optional)
+#     Name of the library in the IDK. Defaults to `output_name`.
+#
+#   output_name (optional)
+#     The tool's name. Defaults to `sdk_name` if specified or target_name.
+#
+#   sources, deps, etc.
+#     Usual GN meaning.
+template("sdk_rustc_binary_host_tool") {
+  assert(defined(invoker.category), "Must define an SDK category")
+  assert(is_host)
+
+  tool_name = target_name
+
+  rustc_binary(tool_name) {
+    forward_variables_from(invoker, "*", _idk_only_parameters)
+  }
+
+  sdk_host_tool(target_name + "_sdk") {
+    forward_variables_from(invoker, _idk_only_parameters)
+    deps = [ ":${tool_name}" ]
+  }
+}
diff --git a/build/tools/bazel2gn/bazel2gn.go b/build/tools/bazel2gn/bazel2gn.go
index 293cfa4..d96cc8e 100644
--- a/build/tools/bazel2gn/bazel2gn.go
+++ b/build/tools/bazel2gn/bazel2gn.go
@@ -261,18 +261,6 @@
 			}
 			name = strings.Join(lines, "\n")
 
-			// Handle differences in naming conventions.
-			if bazelRule == "idk_host_tool" {
-				// In GN, the template did not automatically append "_sdk" to
-				// the name of the atom target and it was included in the name
-				// passed to the template. In Bazel, the macro is consistent
-				// with other IDK atom macros. Handle this by appending "_sdk".
-				if !(len(name) > 1 && name[len(name)-1] == '"') {
-					return nil, fmt.Errorf("expected a quoted string for name, but got %s", name)
-				}
-				name = name[:len(name)-1] + "_sdk\""
-			}
-
 			continue
 		}
 		if ident.Name == "target_compatible_with" {
diff --git a/build/tools/bazel2gn/constants.go b/build/tools/bazel2gn/constants.go
index 0f4da93..e9b700f 100644
--- a/build/tools/bazel2gn/constants.go
+++ b/build/tools/bazel2gn/constants.go
@@ -81,15 +81,15 @@
 	"install_host_tools":  "install_host_tools",
 
 	// IDK
-	"idk_cc_shared_library":    "sdk_shared_library",
-	"idk_cc_shared_library_zx": "zx_library", // With `sdk="shared"` and `sdk_publishable = "partner"`.
-	"idk_cc_source_library":    "sdk_source_set",
-	"idk_cc_source_library_zx": "zx_library", // With `sdk="source"` and `sdk_publishable = "partner"`.
-	"idk_cc_static_library":    "sdk_static_library",
-	"idk_cc_static_library_zx": "zx_library", // With `sdk="static"` and `sdk_publishable = "partner"`.
-	"idk_host_tool":            "sdk_host_tool",
-	"idk_cc_binary_host_tool":  "sdk_executable_host_tool",
-	"idk_go_binary_host_tool":  "sdk_go_binary_host_tool",
+	"idk_cc_shared_library":      "sdk_shared_library",
+	"idk_cc_shared_library_zx":   "zx_library", // With `sdk="shared"` and `sdk_publishable = "partner"`.
+	"idk_cc_source_library":      "sdk_source_set",
+	"idk_cc_source_library_zx":   "zx_library", // With `sdk="source"` and `sdk_publishable = "partner"`.
+	"idk_cc_static_library":      "sdk_static_library",
+	"idk_cc_static_library_zx":   "zx_library", // With `sdk="static"` and `sdk_publishable = "partner"`.
+	"idk_cc_binary_host_tool":    "sdk_executable_host_tool",
+	"idk_go_binary_host_tool":    "sdk_go_binary_host_tool",
+	"idk_rustc_binary_host_tool": "sdk_rustc_binary_host_tool",
 
 	// Other
 	"fidlgentest_go_test": "fidlgentest_go_test",
@@ -165,9 +165,9 @@
 	"rustc_flags":          "rustflags",
 }
 
-// rustBinMap maps from attribute name in Bazel Rust binary rules to GN parameter names.
+// rustBinAttrMap maps from attribute name in Bazel Rust binary rules to GN parameter names.
 // This map only includes attributes that have different names in Bazel and GN.
-var rustBinMap = mustMergeMaps(rustCommonAttrMap, map[string]string{
+var rustBinAttrMap = mustMergeMaps(rustCommonAttrMap, map[string]string{
 	"crate_name": "output_name",
 })
 
@@ -218,11 +218,6 @@
 	"category": "sdk_publishable",
 }
 
-// hostToolAttrMap maps from attribute name in Bazel host tool rules to GN parameter names.
-var hostToolAttrMap = map[string]string{
-	"tool": "deps",
-}
-
 // installHostToolAttrMap maps from attribute name in Bazel install host tool rules to GN parameter names.
 var installHostToolAttrMap = map[string]string{
 	"implementation_deps": "deps",
@@ -243,8 +238,8 @@
 // idkFIDLAttrMap maps from attribute name in Bazel IDK FIDL rules to GN parameter names.
 var idkFIDLAttrMap = mustMergeMaps(idkAttrMap, fidlAttrMap)
 
-// idkHostToolAttrMap maps from attribute name in Bazel IDK host tool rules to GN parameter names.
-var idkHostToolAttrMap = mustMergeMaps(idkAttrMap, hostToolAttrMap)
+// idkRustBinAttrMap maps from attribute name in Bazel IDK Rust binary rules to GN parameter names.
+var idkRustBinAttrMap = mustMergeMaps(idkAttrMap, rustBinAttrMap)
 
 // A mapping from Bazel rule names to attribute mappings.
 // Attribute mappings map from Bazel rule attributes that use different names in GN.
@@ -260,10 +255,10 @@
 	"cc_static_library_zx": ccLibAttrMap,
 
 	// Rust
-	"rust_binary":     rustBinMap,
+	"rust_binary":     rustBinAttrMap,
 	"rust_library":    rustCommonAttrMap,
 	"rust_proc_macro": rustCommonAttrMap,
-	"rustc_binary":    rustBinMap,
+	"rustc_binary":    rustBinAttrMap,
 	"rustc_library":   rustCommonAttrMap,
 	"rustc_test":      rustCommonAttrMap,
 
@@ -272,15 +267,15 @@
 	"zither_fidl_library": fidlAttrMap,
 
 	// IDK
-	"idk_cc_shared_library":    idkCcAttrMap,
-	"idk_cc_shared_library_zx": idkZxAttrMap,
-	"idk_cc_source_library":    idkCcAttrMap,
-	"idk_cc_source_library_zx": idkZxAttrMap,
-	"idk_cc_static_library":    idkCcAttrMap,
-	"idk_cc_static_library_zx": idkZxAttrMap,
-	"idk_host_tool":            idkHostToolAttrMap,
-	"idk_cc_binary_host_tool":  idkAttrMap,
-	"idk_go_binary_host_tool":  idkAttrMap,
+	"idk_cc_shared_library":      idkCcAttrMap,
+	"idk_cc_shared_library_zx":   idkZxAttrMap,
+	"idk_cc_source_library":      idkCcAttrMap,
+	"idk_cc_source_library_zx":   idkZxAttrMap,
+	"idk_cc_static_library":      idkCcAttrMap,
+	"idk_cc_static_library_zx":   idkZxAttrMap,
+	"idk_cc_binary_host_tool":    idkAttrMap,
+	"idk_go_binary_host_tool":    idkAttrMap,
+	"idk_rustc_binary_host_tool": idkRustBinAttrMap,
 
 	// Tools
 	"install_host_tools": installHostToolAttrMap,
diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn
index 875309e..2b218cb 100644
--- a/sdk/BUILD.gn
+++ b/sdk/BUILD.gn
@@ -479,7 +479,7 @@
       "//src/developer/ffx/tools/starnix:sdk",
       "//src/performance/trace2json:bin_sdk",
       "//src/sys/pkg/bin/far:bin_sdk",
-      "//src/sys/pkg/testing/fake-omaha-client:bin_sdk",
+      "//src/sys/pkg/testing/fake-omaha-client:fake-omaha-client_sdk",
       "//tools/fidlcat:fidlcat_sdk",
       "//tools/net/device-finder:device-finder_sdk",
     ]
diff --git a/src/sys/pkg/testing/fake-omaha-client/BUILD.bazel b/src/sys/pkg/testing/fake-omaha-client/BUILD.bazel
index fa6b3896..2cfa161 100644
--- a/src/sys/pkg/testing/fake-omaha-client/BUILD.bazel
+++ b/src/sys/pkg/testing/fake-omaha-client/BUILD.bazel
@@ -3,15 +3,17 @@
 # found in the LICENSE file.
 
 load("@platforms//host:constraints.bzl", "HOST_CONSTRAINTS")
-load("//build/bazel/bazel_idk:defs.bzl", "idk_host_tool")
-load("//build/bazel/rules/rust:defs.bzl", "rustc_binary")
+load("//build/bazel/bazel_idk:defs.bzl", "idk_rustc_binary_host_tool")
 
-rustc_binary(
+idk_rustc_binary_host_tool(
     name = "fake-omaha-client",
     srcs = [
         "src/main.rs",
     ],
+    api_area = "Testing",
+    category = "partner",
     edition = "2024",
+    idk_name = "fake-omaha-client",
     target_compatible_with = HOST_CONSTRAINTS,
     with_host_unit_tests = True,
     deps = [
@@ -24,12 +26,3 @@
         "//third_party/rust_crates/vendor:omaha_client",
     ],
 )
-
-idk_host_tool(
-    name = "bin",
-    api_area = "Testing",
-    category = "partner",
-    idk_name = "fake-omaha-client",
-    target_compatible_with = HOST_CONSTRAINTS,
-    tool = [":fake-omaha-client"],
-)
diff --git a/src/sys/pkg/testing/fake-omaha-client/BUILD.gn b/src/sys/pkg/testing/fake-omaha-client/BUILD.gn
index e19e4a8..7fe4d97 100644
--- a/src/sys/pkg/testing/fake-omaha-client/BUILD.gn
+++ b/src/sys/pkg/testing/fake-omaha-client/BUILD.gn
@@ -3,7 +3,6 @@
 # found in the LICENSE file.
 
 import("//build/components.gni")
-import("//build/rust/rustc_binary.gni")
 import("//build/sdk/sdk_host_tool.gni")
 
 group("fake-omaha-client-host") {
@@ -42,9 +41,12 @@
 }
 
 if (is_host) {
-  rustc_binary("fake-omaha-client") {
+  sdk_rustc_binary_host_tool("fake-omaha-client") {
     sources = [ "src/main.rs" ]
+    sdk_area = "Testing"
+    category = "partner"
     edition = "2024"
+    sdk_name = "fake-omaha-client"
     with_unit_tests = true
     deps = [
       "//src/lib/fuchsia-async",
@@ -57,11 +59,3 @@
     ]
   }
 }
-if (is_host) {
-  sdk_host_tool("bin_sdk") {
-    sdk_area = "Testing"
-    category = "partner"
-    sdk_name = "fake-omaha-client"
-    deps = [ ":fake-omaha-client" ]
-  }
-}