Implement exec_rule.

This is similar to the exec() rule, but allows caller to specify
a custom transition.

Test: TH
Bug: 264710236
Change-Id: I4f9419a750502edaacf1acc20e3a3f69e3d38a05
diff --git a/exec/exec.bzl b/exec/exec.bzl
index ee18f94..a561939 100644
--- a/exec/exec.bzl
+++ b/exec/exec.bzl
@@ -15,6 +15,8 @@
 load("@bazel_skylib//lib:shell.bzl", "shell")
 load(":exec_aspect.bzl", "ExecAspectInfo", "exec_aspect")
 
+_DEFAULT_HASHBANG = "/bin/bash -e"
+
 def _impl(ctx):
     out_file = ctx.actions.declare_file(ctx.label.name)
 
@@ -54,7 +56,7 @@
 [`embedded_exec`](#embedded_exec) to wrap the depended target so its env and args
 are preserved.
 """),
-        "hashbang": attr.string(default = "/bin/bash -e", doc = "Hashbang of the script."),
+        "hashbang": attr.string(default = _DEFAULT_HASHBANG, doc = "Hashbang of the script."),
         "script": attr.string(doc = """The script.
 
 Use `$(rootpath <label>)` to refer to the path of a target specified in `data`. See
@@ -81,7 +83,7 @@
 [`embedded_exec`](#embedded_exec) to wrap the depended target so its env and args
 are preserved.
 """),
-        "hashbang": attr.string(default = "/bin/bash -e", doc = "Hashbang of the script."),
+        "hashbang": attr.string(default = _DEFAULT_HASHBANG, doc = "Hashbang of the script."),
         "script": attr.string(doc = """The script.
 
 Use `$(rootpath <label>)` to refer to the path of a target specified in `data`. See
@@ -94,3 +96,33 @@
     },
     test = True,
 )
+
+def exec_rule(
+        cfg = None,
+        attrs = None):
+    """Returns a rule() that is similar to `exec`, but with the given incoming transition.
+
+    Args:
+        cfg: [Incoming edge transition](https://bazel.build/extending/config#incoming-edge-transitions)
+            on the rule
+        attrs: Additional attributes to be added to the rule.
+
+            Specify `_allowlist_function_transition` if you need a transition.
+    """
+
+    fixed_attrs = {
+        "data": attr.label_list(aspects = [exec_aspect], allow_files = True),
+        "hashbang": attr.string(default = _DEFAULT_HASHBANG),
+        "script": attr.string(),
+    }
+
+    if attrs == None:
+        attrs = {}
+    attrs = attrs | fixed_attrs
+
+    return rule(
+        implementation = _impl,
+        attrs = attrs,
+        cfg = cfg,
+        executable = True,
+    )