Remove fuchsia_runnable.

This also removes support for fuchsia[_test]_package:deps and
fuchsia_test_package:components.

Also register debug symbols for :pkg.publish workflow.

Also propagate far files for all package variants

Fixed: 114603, 117942
Change-Id: I4c263835e54f1db5a2e6316bf39c4643a237c732
Reviewed-on: https://fuchsia-review.googlesource.com/c/sdk-integration/+/787162
Commit-Queue: Darren Chan <chandarren@google.com>
Reviewed-by: Jiaming Li <lijiaming@google.com>
Reviewed-by: Chase Latta <chaselatta@google.com>
diff --git a/bazel_rules_fuchsia/fuchsia/defs.bzl b/bazel_rules_fuchsia/fuchsia/defs.bzl
index 7c11ba1..73b2746 100644
--- a/bazel_rules_fuchsia/fuchsia/defs.bzl
+++ b/bazel_rules_fuchsia/fuchsia/defs.bzl
@@ -62,6 +62,7 @@
     "//fuchsia/private:fuchsia_package.bzl",
     _fuchsia_package = "fuchsia_package",
     _fuchsia_test_package = "fuchsia_test_package",
+    _fuchsia_unittest_package = "fuchsia_unittest_package",
 )
 load(
     "//fuchsia/private:fuchsia_rust.bzl",
@@ -134,6 +135,7 @@
 fuchsia_fidl_llcpp_library = _fuchsia_fidl_llcpp_library
 fuchsia_package = _fuchsia_package
 fuchsia_test_package = _fuchsia_test_package
+fuchsia_unittest_package = _fuchsia_unittest_package
 fuchsia_package_resource = _fuchsia_package_resource
 fuchsia_package_resource_group = _fuchsia_package_resource_group
 fuchsia_package_repository = _fuchsia_package_repository
diff --git a/bazel_rules_fuchsia/fuchsia/private/fuchsia_cc.bzl b/bazel_rules_fuchsia/fuchsia/private/fuchsia_cc.bzl
index 59e597e..0a6a8a4 100644
--- a/bazel_rules_fuchsia/fuchsia/private/fuchsia_cc.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/fuchsia_cc.bzl
@@ -21,8 +21,8 @@
     ":providers.bzl",
     "FuchsiaComponentInfo",
     "FuchsiaDebugSymbolInfo",
-    "FuchsiaDefaultComponentInfo",
     "FuchsiaPackageResourcesInfo",
+    "FuchsiaUnitTestComponentInfo",
 )
 
 KNOWN_PROVIDERS = [
@@ -264,23 +264,23 @@
     },
 )
 
-def _add_default_component_info_for_test_impl(ctx):
+def _add_component_info_for_unit_test_impl(ctx):
     return forward_providers(ctx, ctx.attr.base) + ([
-        FuchsiaDefaultComponentInfo(component = ctx.attr.default_component),
-    ] if ctx.attr.default_component else [])
+        FuchsiaUnitTestComponentInfo(test_component = ctx.attr.generated_component),
+    ] if ctx.attr.generated_component else [])
 
-_add_default_component_info_for_test = rule_variant(
+_add_component_info_for_unit_test = rule_variant(
     variant = "test",
-    implementation = _add_default_component_info_for_test_impl,
-    doc = """Provides FuchsiaDefaultComponentInfo on top of _fuchsia_cc providers.""",
+    implementation = _add_component_info_for_unit_test_impl,
+    doc = """Provides FuchsiaUnitTestComponentInfo on top of _fuchsia_cc providers.""",
     attrs = {
         "base": attr.label(
             doc = "The base _fuchsia_cc target.",
             mandatory = True,
             providers = [[CcInfo, FuchsiaPackageResourcesInfo]],
         ),
-        "default_component": attr.label(
-            doc = "The default component target.",
+        "generated_component": attr.label(
+            doc = "The autogenerated test component.",
             providers = [FuchsiaComponentInfo],
         ),
     },
@@ -354,10 +354,10 @@
         **kwargs
     )
 
-    _add_default_component_info_for_test(
+    _add_component_info_for_unit_test(
         name = name,
         base = ":%s_native_cc" % name,
-        default_component = if_fuchsia(":%s_autogen_component" % name, if_not = None),
+        generated_component = if_fuchsia(":%s_autogen_component" % name, if_not = None),
         testonly = True,
         **kwargs
     )
diff --git a/bazel_rules_fuchsia/fuchsia/private/fuchsia_component.bzl b/bazel_rules_fuchsia/fuchsia/private/fuchsia_component.bzl
index bd9b6fa..86e1f7e 100644
--- a/bazel_rules_fuchsia/fuchsia/private/fuchsia_component.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/fuchsia_component.bzl
@@ -3,8 +3,8 @@
 # found in the LICENSE file.
 
 load(":fuchsia_debug_symbols.bzl", "collect_debug_symbols")
-load(":providers.bzl", "FuchsiaComponentInfo", "FuchsiaPackageResourcesInfo")
-load(":utils.bzl", "make_resource_struct", "rule_variants")
+load(":providers.bzl", "FuchsiaComponentInfo", "FuchsiaPackageResourcesInfo", "FuchsiaUnitTestComponentInfo")
+load(":utils.bzl", "make_resource_struct", "rule_variant", "rule_variants")
 
 def fuchsia_component(name, manifest, deps = None, **kwargs):
     """Creates a Fuchsia component that can be added to a package.
@@ -139,3 +139,18 @@
         ),
     },
 )
+
+def _fuchsia_component_for_unit_test_impl(ctx):
+    return [ctx.attr.unit_test[FuchsiaUnitTestComponentInfo].test_component[FuchsiaComponentInfo]]
+
+fuchsia_component_for_unit_test = rule_variant(
+    variant = "test",
+    doc = """Transforms a FuchsiaUnitTestComponentInfo into a test component.""",
+    implementation = _fuchsia_component_for_unit_test_impl,
+    attrs = {
+        "unit_test": attr.label(
+            doc = "The unit test to convert into a test component",
+            providers = [FuchsiaUnitTestComponentInfo],
+        ),
+    },
+)
diff --git a/bazel_rules_fuchsia/fuchsia/private/fuchsia_debug_symbols.bzl b/bazel_rules_fuchsia/fuchsia/private/fuchsia_debug_symbols.bzl
index 3a2d953..69270e2 100644
--- a/bazel_rules_fuchsia/fuchsia/private/fuchsia_debug_symbols.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/fuchsia_debug_symbols.bzl
@@ -89,27 +89,27 @@
     },
 )
 
-def collect_debug_symbols(*targets):
-    targets = [target for target in flatten(targets) if FuchsiaDebugSymbolInfo in target]
+def collect_debug_symbols(*targets_or_providers):
+    build_id_dirs = [
+        (target_or_provider if (
+            hasattr(target_or_provider, "build_id_dirs")
+        ) else target_or_provider[FuchsiaDebugSymbolInfo]).build_id_dirs
+        for target_or_provider in flatten(targets_or_providers)
+        if hasattr(target_or_provider, "build_id_dirs") or FuchsiaDebugSymbolInfo in target_or_provider
+    ]
     return FuchsiaDebugSymbolInfo(build_id_dirs = {
         build_dir: depset(transitive = [
-            target[FuchsiaDebugSymbolInfo].build_id_dirs[build_dir]
-            for target in targets
-            if build_dir in target[FuchsiaDebugSymbolInfo].build_id_dirs
+            build_dir_mapping[build_dir]
+            for build_dir_mapping in build_id_dirs
+            if build_dir in build_dir_mapping
         ])
         for build_dir in depset([
             file
-            for file in flatten([
-                target[FuchsiaDebugSymbolInfo].build_id_dirs.keys()
-                for target in targets
-            ])
+            for file in flatten([build_id_dir.keys() for build_id_dir in build_id_dirs])
             if type(file) == "File"
         ]).to_list() + depset([
             string
-            for string in flatten([
-                target[FuchsiaDebugSymbolInfo].build_id_dirs.keys()
-                for target in targets
-            ])
+            for string in flatten([build_id_dir.keys() for build_id_dir in build_id_dirs])
             if type(string) == "string"
         ]).to_list()
     })
diff --git a/bazel_rules_fuchsia/fuchsia/private/fuchsia_driver_tool.bzl b/bazel_rules_fuchsia/fuchsia/private/fuchsia_driver_tool.bzl
index 7e80e66..c604abe 100644
--- a/bazel_rules_fuchsia/fuchsia/private/fuchsia_driver_tool.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/fuchsia_driver_tool.bzl
@@ -59,7 +59,7 @@
 
     fuchsia_package(
         name = "pkg",
-        deps = [ ":my_tool" ]
+        tools = [ ":my_tool" ]
     )
 
     $ bazel run //pkg.my_tool -- --arg1 foo --arg2 bar
diff --git a/bazel_rules_fuchsia/fuchsia/private/fuchsia_package.bzl b/bazel_rules_fuchsia/fuchsia/private/fuchsia_package.bzl
index dbc28c4..8b05773 100644
--- a/bazel_rules_fuchsia/fuchsia/private/fuchsia_package.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/fuchsia_package.bzl
@@ -5,38 +5,25 @@
 load(
     ":providers.bzl",
     "FuchsiaComponentInfo",
-    "FuchsiaDebugSymbolInfo",
-    "FuchsiaDefaultComponentInfo",
     "FuchsiaDriverToolInfo",
     "FuchsiaPackageInfo",
     "FuchsiaPackageResourcesInfo",
 )
-load(":fuchsia_debug_symbols.bzl", "get_build_id_dirs", "strip_resources")
-load(":fuchsia_runnable.bzl", "fuchsia_runnable", "fuchsia_runnable_test")
+load(":fuchsia_component.bzl", "fuchsia_component_for_unit_test")
+load(":fuchsia_debug_symbols.bzl", "collect_debug_symbols", "get_build_id_dirs", "strip_resources")
 load(":fuchsia_transition.bzl", "fuchsia_transition")
 load(":package_publishing.bzl", "package_repo_path_from_label", "publish_package")
-load(":utils.bzl", "forward_providers", "make_resource_struct", "rule_variant", "rule_variants", "stub_executable")
-load("@rules_fuchsia//fuchsia/private/workflows:fuchsia_task_publish.bzl", "fuchsia_task_publish")
-load("@rules_fuchsia//fuchsia/private/workflows:fuchsia_workflow.bzl", "fuchsia_workflow")
-load("@rules_fuchsia//fuchsia/private/workflows:fuchsia_task_register_debug_symbols.bzl", "fuchsia_task_register_debug_symbols")
-load("@rules_fuchsia//fuchsia/private/workflows:fuchsia_shell_task.bzl", "fuchsia_shell_task")
-load("@rules_fuchsia//fuchsia/private/workflows:providers.bzl", "FuchsiaWorkflowInfo")
-
-def _dep_name(dep):
-    # convert the dependency to a single word
-    # //foo/bar -> bar
-    # :bar -> bar
-    # //foo:bar -> bar
-    return dep.split("/")[-1].split(":")[-1]
+load(":utils.bzl", "label_name", "make_resource_struct", "rule_variants", "stub_executable")
+load("//fuchsia/private/workflows:fuchsia_package_tasks.bzl", "fuchsia_package_tasks")
 
 def fuchsia_package(
+        *,
         name,
         package_name = None,
         archive_name = None,
         components = [],
         resources = [],
         tools = [],
-        deps = [],
         **kwargs):
     """Builds a fuchsia package.
 
@@ -57,6 +44,7 @@
         tools = [":my_tool"]
     )
     ```
+    - pkg.help: Calling run on this target will show the valid macro-expanded targets
     - pkg.publish: Calling run on this target will publish the package
     - pkg.my_component: Calling run on this target will call `ffx component run`
         with the  component url if it is fuchsia_component instance and will
@@ -72,75 +60,109 @@
         resources: A list of additional resources to add to this package. These
           resources will not have debug symbols stripped.
         tools: Additional tools that should be added to this package.
-        deps: (Deprecated) use components, resources and tools instead.
         package_name: An optional name to use for this package, defaults to name.
         archive_name: An option name for the far file.
         **kwargs: extra attributes to pass along to the build rule.
     """
-    if deps != []:
-        print("""
-\nNOTICE: fuchsia_package:deps is deprecated and will be removed soon.
-Use components, resources and tools instead.
-        """)  # buildifier: disable=print
-
-    # We need to soft transition away from deps. For now, just change the public
-    # API but keep all the existing functionality.
-    deps = deps + components + resources + tools
-
     _build_fuchsia_package(
-        name = name,
-        deps = deps,
-        package_name = package_name,
+        name = "%s_fuchsia_package" % name,
+        components = components,
+        resources = resources,
+        tools = tools,
+        package_name = package_name or name,
         archive_name = archive_name,
         **kwargs
     )
 
-    fuchsia_task_publish(
-        name = "%s.publish" % name,
-        packages = [name],
-        apply_fuchsia_transition = True,
+    fuchsia_package_tasks(
+        name = name,
+        package = "%s_fuchsia_package" % name,
+        components = {component: component for component in components},
+        tools = {tool: tool for tool in tools},
         **kwargs
     )
 
-    if "package_repository_name" in kwargs:
-        kwargs.pop("package_repository_name")
+def _fuchsia_test_package(
+        *,
+        name,
+        package_name = None,
+        archive_name = None,
+        resources = [],
+        _test_component_mapping,
+        _components = [],
+        **kwargs):
+    """Defines test variants of fuchsia_package.
 
-    fuchsia_task_register_debug_symbols(
-        name = "%s.debug_symbols" % name,
-        deps = deps + [name, "@fuchsia_sdk//:debug_symbols"],
-        apply_fuchsia_transition = True,
+    See fuchsia_package for argument descriptions."""
+
+    _build_fuchsia_package_test(
+        name = "%s_fuchsia_package" % name,
+        test_components = _test_component_mapping.values(),
+        components = _components,
+        resources = resources,
+        package_name = package_name or name,
+        archive_name = archive_name,
         **kwargs
     )
 
-    # create targets for each component & tool
-    for dep in components + tools or deps:
-        child_name = name + "." + _dep_name(dep)
-        fuchsia_runnable(
-            name = "%s_runnable" % child_name,
-            package = name,
-            target = dep,
+    fuchsia_package_tasks(
+        name = name,
+        package = "%s_fuchsia_package" % name,
+        components = _test_component_mapping,
+        is_test = True,
+        **kwargs
+    )
+
+def fuchsia_test_package(
+        *,
+        name,
+        test_components = [],
+        components = [],
+        **kwargs):
+    """A test variant of fuchsia_package.
+
+    See _fuchsia_test_package for additional arguments."""
+    _fuchsia_test_package(
+        name = name,
+        _test_component_mapping = {component: component for component in test_components},
+        _components = components,
+        **kwargs
+    )
+
+def fuchsia_unittest_package(
+        *,
+        name,
+        package_name = None,
+        archive_name = None,
+        resources = [],
+        unit_tests,
+        **kwargs):
+    """A variant of fuchsia_test_package containing unit tests.
+
+    See _fuchsia_test_package for additional arguments."""
+
+    test_component_mapping = {}
+    for unit_test in unit_tests:
+        test_component_mapping[unit_test] = "%s_unit_test" % label_name(unit_test)
+        fuchsia_component_for_unit_test(
+            name = test_component_mapping[unit_test],
+            unit_test = unit_test,
             **kwargs
         )
-        fuchsia_shell_task(
-            name = "%s.run" % child_name,
-            default_argument_scope = "global",
-            target = "%s_runnable" % child_name,
-            **kwargs
-        )
-        fuchsia_workflow(
-            name = child_name,
-            sequence = [
-                "%s.debug_symbols" % name,
-                "%s.run" % child_name,
-            ],
-            apply_fuchsia_transition = True,
-            **kwargs
-        )
+
+    _fuchsia_test_package(
+        name = name,
+        package_name = package_name,
+        archive_name = archive_name,
+        resources = resources,
+        _test_component_mapping = test_component_mapping,
+        **kwargs
+    )
+    pass
 
 def _build_fuchsia_package_impl(ctx):
     sdk = ctx.toolchains["@rules_fuchsia//fuchsia:toolchain"]
-    package_name = ctx.attr.package_name or ctx.label.name
-    archive_name = ctx.attr.archive_name or package_name
+    archive_name = ctx.attr.archive_name or ctx.attr.package_name
 
     if not archive_name.endswith(".far"):
         archive_name += ".far"
@@ -174,11 +196,16 @@
     components = []
     drivers = []
 
-    # Collect all the resources from the deps
-    for dep in ctx.attr.deps:
-        if FuchsiaDefaultComponentInfo in dep:
-            dep = dep[FuchsiaDefaultComponentInfo].component
+    # Verify correctness of test vs non-test components.
+    for test_component in ctx.attr.test_components:
+        if not test_component[FuchsiaComponentInfo].is_test:
+            fail("Please use `components` for non-test components.")
+    for component in ctx.attr.components:
+        if component[FuchsiaComponentInfo].is_test:
+            fail("Please use `test_components` for test components.")
 
+    # Collect all the resources from the deps
+    for dep in ctx.attr.test_components + ctx.attr.components + ctx.attr.resources + ctx.attr.tools:
         if FuchsiaComponentInfo in dep:
             component_info = dep[FuchsiaComponentInfo]
             component_manifest = component_info.manifest
@@ -196,15 +223,16 @@
                 ),
             )
             resources_to_strip.extend([r for r in component_info.resources])
-        elif FuchsiaPackageResourcesInfo in dep:
-            resources_to_strip.extend(dep[FuchsiaPackageResourcesInfo].resources)
         elif FuchsiaDriverToolInfo in dep:
             resources_to_strip.extend(dep[FuchsiaDriverToolInfo].resources)
+        elif FuchsiaPackageResourcesInfo in dep:
+            # Don't strip debug symbols from resources.
+            package_resources.extend(dep[FuchsiaPackageResourcesInfo].resources)
         else:
             fail("Unknown dependency type being added to package: %s" % dep.label)
 
     # Grab all of our stripped resources
-    stripped_resources, debug_info = strip_resources(ctx, resources_to_strip)
+    stripped_resources, _debug_info = strip_resources(ctx, resources_to_strip)
     package_resources.extend(stripped_resources)
 
     # Write our package_manifest file
@@ -221,7 +249,7 @@
             "-o",  # output directory
             output_dir,
             "-n",  # name of the package
-            package_name,
+            ctx.attr.package_name,
             "init",
         ],
         outputs = [
@@ -249,7 +277,7 @@
             "-m",
             manifest.path,
             "-n",
-            package_name,
+            ctx.attr.package_name,
         ] + repo_name_args + api_level_input + [
             "build",
             "--output-package-manifest",
@@ -273,7 +301,7 @@
             "-m",
             manifest.path,
             "-n",
-            package_name,
+            ctx.attr.package_name,
             "archive",
             "-output",
             # pm automatically adds .far so we have to remove it here to make
@@ -316,16 +344,23 @@
             far_file = far_file,
             package_manifest = output_package_manifest,
             files = [output_package_manifest, meta_far] + build_inputs,
-            package_name = package_name,
+            package_name = ctx.attr.package_name,
             components = components,
             drivers = drivers,
             meta_far = meta_far,
             package_resources = package_resources,
 
             # TODO: Remove this field, change usages to FuchsiaDebugSymbolInfo.
-            build_id_dir = get_build_id_dirs(debug_info)[0],
+            build_id_dir = get_build_id_dirs(_debug_info)[0],
         ),
-        debug_info,
+        collect_debug_symbols(
+            _debug_info,
+            ctx.attr.test_components,
+            ctx.attr.components,
+            ctx.attr.resources,
+            ctx.attr.tools,
+            ctx.attr._fuchsia_sdk_debug_symbols,
+        ),
     ]
 
 _build_fuchsia_package, _build_fuchsia_package_test = rule_variants(
@@ -335,20 +370,37 @@
     cfg = fuchsia_transition,
     toolchains = ["@rules_fuchsia//fuchsia:toolchain", "@bazel_tools//tools/cpp:toolchain_type"],
     attrs = {
+        "package_name": attr.string(
+            doc = "The name of the package",
+            mandatory = True,
+        ),
         "archive_name": attr.string(
             doc = "What to name the archive. The .far file will be appended if not in this name. Defaults to package_name",
         ),
-        "package_name": attr.string(
-            doc = "The name of the package, defaults to the rule name",
-        ),
         # TODO(https://fxbug.dev/114334): Improve doc for this field when we
         # have more clarity from the bug.
         "package_repository_name": attr.string(
             doc = "Repository name of this package, defaults to None",
         ),
-        "deps": attr.label_list(
-            doc = "The list of dependencies this package depends on",
-            providers = [[FuchsiaComponentInfo], [FuchsiaDefaultComponentInfo], [FuchsiaPackageResourcesInfo], [FuchsiaDriverToolInfo]],
+        "components": attr.label_list(
+            doc = "The list of components included in this package",
+            providers = [FuchsiaComponentInfo],
+        ),
+        "test_components": attr.label_list(
+            doc = "The list of test components included in this package",
+            providers = [FuchsiaComponentInfo],
+        ),
+        "resources": attr.label_list(
+            doc = "The list of resources included in this package",
+            providers = [FuchsiaPackageResourcesInfo],
+        ),
+        "tools": attr.label_list(
+            doc = "The list of tools included in this package",
+            providers = [FuchsiaDriverToolInfo],
+        ),
+        "_fuchsia_sdk_debug_symbols": attr.label(
+            doc = "Include debug symbols from @fuchsia_sdk.",
+            default = "@fuchsia_sdk//:debug_symbols",
         ),
         "_package_repo_path": attr.label(
             doc = "The command line flag used to publish packages.",
@@ -372,139 +424,3 @@
         ),
     },
 )
-
-def _add_package_providers_impl(ctx):
-    return forward_providers(ctx, ctx.attr.workflow) + [
-        ctx.attr.package[FuchsiaPackageInfo],
-        ctx.attr.package[FuchsiaDebugSymbolInfo],
-    ]
-
-_add_package_providers_for_test = rule_variant(
-    variant = "test",
-    doc = "Adds FuchsiaPackageInfo and FuchsiaDebugSymbolInfo on top of a fuchsia_workflow.",
-    implementation = _add_package_providers_impl,
-    cfg = fuchsia_transition,
-    attrs = {
-        "workflow": attr.label(
-            doc = "The base fuchsia_workflow to extend.",
-            providers = [FuchsiaWorkflowInfo],
-            mandatory = True,
-        ),
-        "package": attr.label(
-            doc = "The package to get providers from.",
-            providers = [[FuchsiaPackageInfo, FuchsiaDebugSymbolInfo]],
-            mandatory = True,
-        ),
-        "_allowlist_function_transition": attr.label(
-            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
-        ),
-    },
-)
-
-def fuchsia_test_package(
-        name = None,
-        package_name = None,
-        archive_name = None,
-        test_components = [],
-        components = [],
-        resources = [],
-        tools = [],
-        deps = [],
-        tags = [],
-        **kwargs):
-    """A test variant of fuchsia_package.
-
-    See fuchsia_package for argument descriptions."""
-    if deps != []:
-        print("""
-\nNOTICE: fuchsia_package:deps is deprecated and will be removed soon.
-Use test_components, components, resources and tools instead.
-        """)  # buildifier: disable=print
-
-    # We need to soft transition away from deps. For now, just change the public
-    # API but keep all the existing functionality.
-    deps = deps + test_components + components + resources + tools
-
-    _build_fuchsia_package_test(
-        name = "%s_fuchsia_package" % name,
-        deps = deps,
-        package_name = package_name or name,
-        archive_name = archive_name,
-        tags = tags,
-        **kwargs
-    )
-
-    fuchsia_task_register_debug_symbols(
-        name = "%s.debug_symbols" % name,
-        deps = deps + [
-            "%s_fuchsia_package" % name,
-            "@fuchsia_sdk//:debug_symbols",
-        ],
-        testonly = True,
-        apply_fuchsia_transition = True,
-        tags = tags,
-        **kwargs
-    )
-
-    fuchsia_task_publish(
-        name = "%s.publish" % name,
-        packages = ["%s_fuchsia_package" % name],
-        testonly = True,
-        apply_fuchsia_transition = True,
-        tags = tags,
-        **kwargs
-    )
-
-    # create targets for each component & tool
-    component_test_tasks = []
-    for dep in test_components or deps:
-        child_name = name + "." + _dep_name(dep)
-        fuchsia_runnable_test(
-            name = "%s_runnable" % child_name,
-            package = name + "_fuchsia_package",
-            target = dep,
-            testonly = True,
-            tags = tags,
-            **kwargs
-        )
-        component_test_tasks.append("%s.run" % child_name)
-        fuchsia_shell_task(
-            name = "%s.run" % child_name,
-            default_argument_scope = "global",
-            target = "%s_runnable" % child_name,
-            testonly = True,
-            tags = tags,
-            **kwargs
-        )
-        fuchsia_workflow(
-            name = child_name,
-            sequence = [
-                "%s.debug_symbols" % name,
-                "%s.run" % child_name,
-            ],
-            testonly = True,
-            apply_fuchsia_transition = True,
-            # TODO(fxbug.dev/98996): Use ffx isolation. ffx test run currently needs
-            # to access ~/.local/share/Fuchsia/ffx/ or else it crashes.
-            tags = tags + ["no-sandbox", "no-cache"],
-            **kwargs
-        )
-
-    fuchsia_workflow(
-        name = "%s_workflow" % name,
-        sequence = [
-            "%s.debug_symbols" % name,
-        ] + component_test_tasks,
-        testonly = True,
-        **kwargs
-    )
-
-    _add_package_providers_for_test(
-        name = name,
-        workflow = "%s_workflow" % name,
-        package = "%s_fuchsia_package" % name,
-        # TODO(fxbug.dev/98996): Use ffx isolation. ffx test run currently needs
-        # to access ~/.local/share/Fuchsia/ffx/ or else it crashes.
-        tags = tags + ["no-sandbox", "no-cache", "manual"],
-        **kwargs
-    )
diff --git a/bazel_rules_fuchsia/fuchsia/private/fuchsia_runnable.bzl b/bazel_rules_fuchsia/fuchsia/private/fuchsia_runnable.bzl
deleted file mode 100644
index 1759992..0000000
--- a/bazel_rules_fuchsia/fuchsia/private/fuchsia_runnable.bzl
+++ /dev/null
@@ -1,120 +0,0 @@
-# Copyright 2022 The Fuchsia Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-load(":utils.bzl", "normalized_target_name", "rule_variants")
-load(
-    ":providers.bzl",
-    "FuchsiaComponentInfo",
-    "FuchsiaDefaultComponentInfo",
-    "FuchsiaDriverToolInfo",
-    "FuchsiaPackageInfo",
-    "FuchsiaRunnableInfo",
-)
-
-def _run_as_component(ctx, component):
-    sdk = ctx.toolchains["@rules_fuchsia//fuchsia:toolchain"]
-    executable_file = ctx.actions.declare_file(ctx.label.name + "_run_component.sh")
-
-    package = ctx.attr.package[FuchsiaPackageInfo]
-
-    if component.is_driver:
-        action = "register_driver"
-    else:
-        action = "run_component"
-
-    ctx.actions.expand_template(
-        template = ctx.file._component_run_template,
-        output = executable_file,
-        substitutions = {
-            "{{ffx}}": sdk.ffx.short_path,
-            "{{pm}}": sdk.pm.short_path,
-            "{{repo_path}}": "pm_repo",
-            "{{default_repo_name}}": "bazel." + normalized_target_name(ctx.label),
-            "{{far_file}}": package.far_file.short_path,
-            "{{package_name}}": package.package_name,
-            "{{component_manifest}}": component.manifest.basename,
-            "{{run_or_test_component}}": "test_component" if ctx.attr._variant == "test" else action,
-        },
-        is_executable = True,
-    )
-
-    runfiles = sdk.runfiles.merge(ctx.runfiles([package.far_file]))
-
-    return [
-        DefaultInfo(executable = executable_file, runfiles = runfiles),
-        FuchsiaRunnableInfo(
-            executable = executable_file,
-            runfiles = runfiles,
-            is_test = ctx.attr._variant == "test",
-        ),
-    ]
-
-def _run_driver_tool(ctx, tool):
-    sdk = ctx.toolchains["@rules_fuchsia//fuchsia:toolchain"]
-    executable_file = ctx.actions.declare_file(ctx.label.name + "_run_driver_tool.sh")
-
-    archive = ctx.attr.package[FuchsiaPackageInfo]
-
-    ctx.actions.expand_template(
-        template = ctx.file._driver_tool_run_template,
-        output = executable_file,
-        substitutions = {
-            "{{ffx}}": sdk.ffx.short_path,
-            "{{pm}}": sdk.pm.short_path,
-            "{{repo_path}}": "pm_repo",
-            "{{default_repo_name}}": "bazel." + normalized_target_name(ctx.label),
-            "{{far_file}}": archive.far_file.short_path,
-            "{{package_name}}": archive.package_name,
-            "{{binary}}": tool.dest,
-        },
-        is_executable = True,
-    )
-
-    runfiles = sdk.runfiles.merge(ctx.runfiles([archive.far_file]))
-
-    return [
-        DefaultInfo(executable = executable_file, runfiles = runfiles),
-        FuchsiaRunnableInfo(
-            executable = executable_file,
-            runfiles = runfiles,
-            is_test = False,
-        ),
-    ]
-
-def _fuchsia_runnable_impl(ctx):
-    _target = ctx.attr.target
-    if FuchsiaDefaultComponentInfo in _target:
-        _target = _target[FuchsiaDefaultComponentInfo].component
-
-    if FuchsiaComponentInfo in _target and _target[FuchsiaComponentInfo].is_test == (ctx.attr._variant == "test"):
-        return _run_as_component(ctx, _target[FuchsiaComponentInfo])
-    elif FuchsiaDriverToolInfo in _target:
-        return _run_driver_tool(ctx, _target[FuchsiaDriverToolInfo].binary)
-    else:
-        return []
-
-fuchsia_runnable, fuchsia_runnable_test = rule_variants(
-    variants = ("executable", "test"),
-    doc = "Creates a rule that can be invoked with bazel run/test",
-    implementation = _fuchsia_runnable_impl,
-    toolchains = ["@rules_fuchsia//fuchsia:toolchain"],
-    attrs = {
-        "package": attr.label(
-            doc = "The fuchsia_package containing target",
-            providers = [FuchsiaPackageInfo],
-        ),
-        "target": attr.label(
-            doc = "The target of the runnable action",
-            mandatory = True,
-        ),
-        "_component_run_template": attr.label(
-            default = ":templates/run_component.sh.tmpl",
-            allow_single_file = True,
-        ),
-        "_driver_tool_run_template": attr.label(
-            default = ":templates/run_driver_tool.sh.tmpl",
-            allow_single_file = True,
-        ),
-    },
-)
diff --git a/bazel_rules_fuchsia/fuchsia/private/providers.bzl b/bazel_rules_fuchsia/fuchsia/private/providers.bzl
index ea6b92f..104143d 100644
--- a/bazel_rules_fuchsia/fuchsia/private/providers.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/providers.bzl
@@ -38,10 +38,10 @@
     },
 )
 
-FuchsiaDefaultComponentInfo = provider(
-    "Information that can be used to construct a FuchsiaComponentInfo.",
+FuchsiaUnitTestComponentInfo = provider(
+    "Allows unit tests to be treated as test components.",
     fields = {
-        "component": "The label of a target that provides FuchsiaComponentInfo.",
+        "test_component": "The label of the underlying fuchsia_test_component.",
     },
 )
 
diff --git a/bazel_rules_fuchsia/fuchsia/private/templates/run_component.sh.tmpl b/bazel_rules_fuchsia/fuchsia/private/templates/run_component.sh.tmpl
deleted file mode 100644
index 4a8c40e..0000000
--- a/bazel_rules_fuchsia/fuchsia/private/templates/run_component.sh.tmpl
+++ /dev/null
@@ -1,177 +0,0 @@
-#!/bin/bash
-# Copyright 2022 The Fuchsia Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-set -e -o pipefail
-
-help() {
-  echo
-  echo "runs/tests a component on a the target."
-  echo
-  echo "Usage:"
-  echo "   $(basename "$0") [<options>]"
-  echo
-  echo "Options:"
-  echo
-  echo "  -t, --target <target_name>"
-  echo "     Specifies the target to run the component on. Defaults to ffx target default get"
-  echo "  -v, --verbose"
-  echo "     If set will output more logs"
-  echo "  -s, --session"
-  echo "     If set the component will be added to the session (`ffx session add`) instead"
-  echo "     of running in ffx-laboratory (`ffx component run`)."
-  echo "  --repository-name <repo_name>"
-  echo "     Sets the name of the repository to use when publishing the package."
-  echo "     This command will fail if the repository already exists unless the"
-  echo "     --use-existing flag is also passed."
-  echo "  --use-existing"
-  echo "     Allows for an existing repository to be used. This flag is not yet implemented."
-  echo
-  echo "All other options will be forwarded to ffx component run (when not using --session),"
-  echo "ffx session add (when using --sesssion) or ffx test run."
-  echo
-}
-
-function log {
-  if [[ "$VERBOSE_LOGS" -eq 1 ]]; then
-    echo "$@"
-  fi
-}
-
-function repository_name {
-  if [[ -n "${REPOSITORY_NAME}" ]]; then
-    echo "${REPOSITORY_NAME}"
-  else
-    echo {{default_repo_name}}
-  fi
-}
-
-function component_url {
-  local repo_name=$(repository_name)
-  echo "fuchsia-pkg://${repo_name}/{{package_name}}#meta/{{component_manifest}}"
-}
-
-function ffx_with_target {
-  if [[ -n "${TARGET}" ]]; then
-    {{ffx}} --target "${TARGET}" "$@"
-  else
-    {{ffx}} "$@"
-  fi
-}
-
-function cleanup_repo {
-  # Do not exit if repo_path doesn't exist
-  set +e
-  log "Cleaning up repository"
-  rm -r {{repo_path}}
-  {{ffx}} repository remove "$(repository_name)"
-  set -e
-}
-
-function create_and_register_repo {
-  local repo_name=$(repository_name)
-  log "Creating new repository $(repository_name)"
-
-  check_if_repo_exists
-
-  {{pm}} newrepo -vt -repo {{repo_path}} || exit 1
-
-  {{ffx}} repository add-from-pm -r "${repo_name}" {{repo_path}} || exit 1
-  ffx_with_target target repository register -r "${repo_name}" || exit 1
-}
-
-function publish_far_file {
-  log "Publishing {{far_file}}"
-
-  {{pm}} publish -vt -C -a -f {{far_file}} -repo {{repo_path}} || exit 1
-}
-
-function run_component {
-  local url=$(component_url)
-  log "running component: ${url} $*"
-
-  if [[ "$RUN_IN_SESSION" -eq 1 ]]; then
-    ffx_with_target session add "${url}" "${@}"
-  else
-    # TODO(fxb/113040) - Update moniker to a template variable given to this script by a Bazel rule.
-    ffx_with_target component run "/core/ffx-laboratory:{{component_manifest}}" "${url}" --recreate "${@}"
-  fi
-
-}
-
-function register_driver {
-  local url=$(component_url)
-  log "registering driver: ${url} $*"
-
-  ffx_with_target driver register "${url}" "${@}"
-}
-
-function test_component {
-  local url=$(component_url)
-  log "testing component: ${url} $*"
-
-  ffx_with_target test run "${url}" "${@}"
-}
-
-function check_if_repo_exists {
-  local repo_name=$(repository_name)
-  local result=$({{ffx}} --machine JSON repository list | grep "\"name\":\""${repo_name}"\"" >/dev/null)
-
-
-  if [[ ! -z "${result}" ]]; then
-    echo "ERROR: repository ${repo_name} already exists."
-    echo "  pass the --use-existing if you would like to use an existing repository."
-    exit 1
-  fi
-}
-
-function main {
-  UNUSED_ARGS=()
-  while [[ $# -gt 0 ]]; do
-    case $1 in
-      -t|--target)
-        TARGET="$2"
-        shift # past argument
-        shift # past value
-        ;;
-      -v|--verbose)
-        VERBOSE_LOGS=1
-        shift # past value
-        ;;
-      -h|--help)
-        help
-        exit 1
-        ;;
-      --use-existing)
-        shift # past value
-        echo "ERROR: the --use-existing flag is not yet supported."
-        exit 1
-        ;;
-      --repository-name)
-        REPOSITORY_NAME="$2"
-        shift # past argument
-        shift # past value
-        echo "set repo name"
-        ;;
-      -s|--session)
-        RUN_IN_SESSION=1
-        shift # past value
-        ;;
-      *)
-        UNUSED_ARGS+=("$1") # save unused arg
-        shift # past argument
-        ;;
-    esac
-  done
-  set -- "${UNUSED_ARGS[@]}"
-
-  # make sure we cleanup when we are done
-  trap cleanup_repo EXIT
-
-  create_and_register_repo
-  publish_far_file
-  {{run_or_test_component}} "$@"
-}
-
-main "$@"
diff --git a/bazel_rules_fuchsia/fuchsia/private/templates/run_driver_tool.sh.tmpl b/bazel_rules_fuchsia/fuchsia/private/templates/run_driver_tool.sh.tmpl
deleted file mode 100644
index 74a865c..0000000
--- a/bazel_rules_fuchsia/fuchsia/private/templates/run_driver_tool.sh.tmpl
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/bin/bash
-# Copyright 2022 The Fuchsia Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-set -e -o pipefail
-
-help() {
-  echo
-  echo "runs a driver tool on the target with ffx driver run-tool."
-  echo
-  echo "Usage:"
-  echo "   $(basename "$0") [<options>]"
-  echo
-  echo "Options:"
-  echo
-  echo "  -t, --target <target_name>"
-  echo "     Specifies the target to run the tool on. Defaults to ffx target default get"
-  echo "  -v, --verbose"
-  echo "     If set will output more logs"
-  echo
-  echo "All other options will be forwarded to ffx driver run-tool."
-  echo
-}
-
-function log {
-  if [[ "$VERBOSE_LOGS" -eq 1 ]]; then
-    echo "$@"
-  fi
-}
-
-function repository_name {
-  if [[ -n "${REPOSITORY_NAME}" ]]; then
-    echo "${REPOSITORY_NAME}"
-  else
-    echo {{default_repo_name}}
-  fi
-}
-
-function driver_tool_url {
-  local repo_name=$(repository_name)
-  echo "fuchsia-pkg://${repo_name}/{{package_name}}#{{binary}}"
-}
-
-function ffx_with_target {
-  if [[ -n "${TARGET}" ]]; then
-    {{ffx}} --target "${TARGET}" "$@"
-  else
-    {{ffx}} "$@"
-  fi
-}
-
-function cleanup_repo {
-  # Do not exit if repo_path doesn't exist
-  set +e
-  log "Cleaning up repository"
-  rm -r {{repo_path}}
-  {{ffx}} repository remove "$(repository_name)"
-  set -e
-}
-
-function create_and_register_repo {
-  local repo_name=$(repository_name)
-  log "Creating new repository $(repository_name)"
-
-  check_if_repo_exists
-
-  {{pm}} newrepo -vt -repo {{repo_path}} || exit 1
-
-  {{ffx}} repository add-from-pm -r "${repo_name}" {{repo_path}} || exit 1
-  ffx_with_target target repository register -r "${repo_name}" || exit 1
-}
-
-function publish_far_file {
-  log "Publishing {{far_file}}"
-
-  {{pm}} publish -vt -C -a -f {{far_file}} -repo {{repo_path}} || exit 1
-}
-
-function run_driver_tool {
-  local url=$(driver_tool_url)
-  log "running driver tool: ${url} $*"
-  ffx_with_target driver run-tool "${url}" -- "$@"
-}
-
-function check_if_repo_exists {
-  local repo_name=$(repository_name)
-  local result=$({{ffx}} --machine JSON repository list | grep "\"name\":\""${repo_name}"\"" >/dev/null)
-
-
-  if [[ ! -z "${result}" ]]; then
-    echo "ERROR: repository ${repo_name} already exists."
-    echo "  pass the --use-existing if you would like to use an existing repository."
-    exit 1
-  fi
-}
-
-function main {
-  UNUSED_ARGS=()
-  while [[ $# -gt 0 ]]; do
-    case $1 in
-      -t|--target)
-        TARGET="$2"
-        shift
-        shift
-        ;;
-      -v|--verbose)
-        VERBOSE_LOGS=1
-        shift
-        ;;
-      -h|--help)
-        help
-        exit
-        ;;
-      *)
-        UNUSED_ARGS+=("$1") # save unused arg
-        shift
-        ;;
-    esac
-  done
-  set -- "${UNUSED_ARGS[@]}"
-
-  create_and_register_repo
-  publish_far_file
-  run_driver_tool "$@"
-}
-
-# make sure we cleanup when we are done
-trap cleanup_repo EXIT
-
-main "$@"
diff --git a/bazel_rules_fuchsia/fuchsia/private/utils.bzl b/bazel_rules_fuchsia/fuchsia/private/utils.bzl
index 27e0616..a8aeff3 100644
--- a/bazel_rules_fuchsia/fuchsia/private/utils.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/utils.bzl
@@ -14,12 +14,19 @@
 
 _INVALID_LABEL_CHARACTERS = "\"!%@^_#$&'()*+,;<=>?[]{|}~/".elems()
 
-def normalized_target_name(target):
-    label = target.name.lower()
+def normalized_target_name(label):
+    label = label.lower()
     for c in _INVALID_LABEL_CHARACTERS:
         label = label.replace(c, ".")
     return label
 
+def label_name(label):
+    # convert the label to a single word
+    # //foo/bar -> bar
+    # :bar -> bar
+    # //foo:bar -> bar
+    return label.split("/")[-1].split(":")[-1]
+
 def get_project_execroot(ctx):
     # Gets the project/workspace execroot relative to the output base.
     # See https://bazel.build/docs/output_directories.
@@ -273,6 +280,6 @@
     if (rparts[1] != ".so"):
         return False
     for char in rparts[2].elems():
-        if not (char.isdigit() or char == '.'):
+        if not (char.isdigit() or char == "."):
             return False
     return True
diff --git a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_package_tasks.bzl b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_package_tasks.bzl
new file mode 100644
index 0000000..7178288
--- /dev/null
+++ b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_package_tasks.bzl
@@ -0,0 +1,286 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+load(":fuchsia_shell_task.bzl", "shell_task_rule")
+load(":fuchsia_task_ffx.bzl", "fuchsia_task_ffx")
+load(":fuchsia_task_publish.bzl", "fuchsia_task_publish")
+load(":fuchsia_task_register_debug_symbols.bzl", "fuchsia_task_register_debug_symbols")
+load(":fuchsia_task_run_component.bzl", "fuchsia_task_run_component")
+load(":fuchsia_task_run_driver_tool.bzl", "fuchsia_task_run_driver_tool")
+load(":fuchsia_task_verbs.bzl", "make_help_executable", "verbs")
+load(":fuchsia_workflow.bzl", "fuchsia_workflow", "fuchsia_workflow_rule")
+load(":providers.bzl", "FuchsiaDebugSymbolInfo", "FuchsiaPackageInfo", "FuchsiaWorkflowInfo")
+load(":utils.bzl", "label_name", "normalized_target_name")
+
+def _to_verb(label):
+    return verbs.custom(label_name(label))
+
+def _fuchsia_package_help_impl(ctx, make_shell_task):
+    help = make_help_executable(ctx, dict((
+        [(verbs.noverb, "Run all test components within this test package.")] if ctx.attr.is_test and ctx.attr.components else []
+    ) + [
+        (verbs.help, "Print this help message."),
+        (verbs.debug_symbols, "Register this package's debug symbols."),
+        (verbs.publish, "Publish this package and register debug symbols."),
+    ] + [
+        (_to_verb(component), "Publish this package and run '%s' with debug symbols." % component)
+        for component in ctx.attr.components
+    ] + [
+        (_to_verb(tool), "Publish this package and run '%s' with debug symbols" % tool)
+        for tool in ctx.attr.tools
+    ]), name = ctx.attr.top_level_name)
+    return make_shell_task([help])
+
+(
+    __fuchsia_package_help,
+    _fuchsia_package_help_for_test,
+    _fuchsia_package_help,
+) = shell_task_rule(
+    implementation = _fuchsia_package_help_impl,
+    doc = "Prints valid runnable sub-targets in a package.",
+    attrs = {
+        "is_test": attr.bool(
+            doc = "Whether the package is a test package.",
+            mandatory = True,
+        ),
+        "package": attr.label(
+            doc = "The package.",
+            providers = [FuchsiaPackageInfo],
+            mandatory = True,
+        ),
+        "components": attr.string_list(
+            doc = "The component names.",
+            mandatory = True,
+        ),
+        "tools": attr.string_list(
+            doc = "The driver tool names.",
+            mandatory = True,
+        ),
+        "debug_symbols_task": attr.label(
+            doc = "The debug symbols task associated with the package.",
+            providers = [FuchsiaWorkflowInfo],
+            mandatory = True,
+        ),
+        "publish_task": attr.label(
+            doc = "The package publishing task associated with the package.",
+            providers = [FuchsiaWorkflowInfo],
+            mandatory = True,
+        ),
+        "top_level_name": attr.string(
+            doc = "The top level target name associated with these tasks",
+            mandatory = True,
+        ),
+    },
+)
+
+def _fuchsia_package_default_task_impl(ctx, make_workflow):
+    default_workflow = make_workflow(sequence = [
+        ctx.attr.debug_symbols_task,
+        ctx.attr.publish_task,
+        ctx.attr.publish_task,
+    ] + ctx.attr.component_run_tasks + [
+        ctx.attr.publish_cleanup_task,
+    ] if (
+        ctx.attr.is_test and ctx.attr.component_run_tasks
+    ) else [ctx.attr.help_task])
+    return default_workflow + [
+        ctx.attr.package[FuchsiaPackageInfo],
+        ctx.attr.package[FuchsiaDebugSymbolInfo],
+    ]
+
+(
+    __fuchsia_package_default_task,
+    _fuchsia_package_default_task_for_test,
+    _fuchsia_package_default_task,
+) = fuchsia_workflow_rule(
+    implementation = _fuchsia_package_default_task_impl,
+    doc = "Runs all test components for test packages, or prints a help message.",
+    attrs = {
+        "is_test": attr.bool(
+            doc = "Whether the package is a test package.",
+            mandatory = True,
+        ),
+        "help_task": attr.label(
+            doc = "The help task describing valid package subtargets.",
+            providers = [FuchsiaWorkflowInfo],
+            mandatory = True,
+        ),
+        "debug_symbols_task": attr.label(
+            doc = "The debug symbols task associated with the package.",
+            providers = [FuchsiaWorkflowInfo],
+            mandatory = True,
+        ),
+        "publish_task": attr.label(
+            doc = "The package publishing task associated with the package.",
+            providers = [FuchsiaWorkflowInfo],
+            mandatory = True,
+        ),
+        "publish_cleanup_task": attr.label(
+            doc = "The package publishing cleanup task associated with the package.",
+            providers = [FuchsiaWorkflowInfo],
+            mandatory = True,
+        ),
+        "component_run_tasks": attr.label_list(
+            doc = "The component run tasks.",
+            providers = [FuchsiaWorkflowInfo],
+            mandatory = True,
+        ),
+        "package": attr.label(
+            doc = "The package.",
+            providers = [FuchsiaPackageInfo],
+            mandatory = True,
+        ),
+    },
+)
+
+def fuchsia_package_tasks(
+        *,
+        name,
+        package,
+        components,
+        tools = {},
+        is_test = False,
+        tags = [],
+        **kwargs):
+    # TODO(fxbug.dev/98996): Use ffx isolation. ffx test run currently needs
+    # to access ~/.local/share/Fuchsia/ffx/ or else it crashes.
+    top_level_tags = tags + (["no-sandbox", "no-cache"] if is_test else [])
+
+    # Mark test children as manual.
+    manual_test = ["manual"] if is_test else []
+
+    # Override testonly since it's used to determine test vs non-test rule
+    # variant selection for workflows.
+    kwargs["testonly"] = is_test
+
+    # For `bazel run :pkg.debug_symbols`.
+    debug_symbols_task = verbs.debug_symbols(name)
+    fuchsia_task_register_debug_symbols(
+        name = debug_symbols_task,
+        deps = [package],
+        apply_fuchsia_transition = True,
+        tags = top_level_tags,
+        **kwargs
+    )
+
+    # For `bazel run :pkg.publish`.
+    publish_task = verbs.publish(name)
+    anonymous_publish_task = "%s_anonymous" % publish_task
+    anonymous_repo_name = "bazel.%s" % normalized_target_name(anonymous_publish_task)
+    fuchsia_task_publish(
+        name = anonymous_publish_task,
+        packages = [package],
+        package_repository_name = anonymous_repo_name,
+        **kwargs
+    )
+    fuchsia_task_ffx(
+        name = verbs.delete_repo(anonymous_publish_task),
+        arguments = [
+            "repository",
+            "remove",
+            anonymous_repo_name,
+        ],
+        **kwargs
+    )
+    publish_only_task = "%s_only" % publish_task
+    fuchsia_task_publish(
+        name = publish_only_task,
+        packages = [package],
+        **kwargs
+    )
+    fuchsia_workflow(
+        name = publish_task,
+        sequence = [
+            debug_symbols_task,
+            publish_only_task,
+        ],
+        apply_fuchsia_transition = True,
+        tags = top_level_tags,
+        **kwargs
+    )
+
+    # For `bazel run :pkg.help`.
+    help_task = verbs.help(name)
+    _fuchsia_package_help(
+        name = help_task,
+        package = package,
+        components = components.keys(),
+        tools = tools,
+        debug_symbols_task = debug_symbols_task,
+        publish_task = publish_task,
+        top_level_name = name,
+        is_test = is_test,
+        apply_fuchsia_transition = True,
+        tags = top_level_tags,
+        **kwargs
+    )
+
+    # For `bazel run :pkg.component`.
+    component_run_tasks = []
+    for label, component in components.items():
+        component_run_task = _to_verb(label)(name)
+        component_run_tasks.append("%s.run_only" % component_run_task)
+        fuchsia_task_run_component(
+            name = component_run_tasks[-1],
+            default_argument_scope = "global",
+            repository = anonymous_repo_name,
+            package = package,
+            component = component,
+            tags = tags,
+            **kwargs
+        )
+
+        fuchsia_workflow(
+            name = component_run_task,
+            sequence = [
+                debug_symbols_task,
+                anonymous_publish_task,
+                component_run_tasks[-1],
+                verbs.delete_repo(anonymous_publish_task),
+            ],
+            apply_fuchsia_transition = True,
+            tags = top_level_tags + manual_test,
+            **kwargs
+        )
+
+    # For `bazel run :pkg.tool`.
+    for label, tool in tools.items():
+        tool_run_task = _to_verb(label)(name)
+        fuchsia_task_run_driver_tool(
+            name = "%s.run_only" % tool_run_task,
+            default_argument_scope = "global",
+            repository = anonymous_repo_name,
+            package = package,
+            tool = tool,
+            tags = tags,
+            **kwargs
+        )
+
+        fuchsia_workflow(
+            name = tool_run_task,
+            sequence = [
+                debug_symbols_task,
+                anonymous_publish_task,
+                "%s.run_only" % tool_run_task,
+                verbs.delete_repo(anonymous_publish_task),
+            ],
+            apply_fuchsia_transition = True,
+            tags = top_level_tags,
+            **kwargs
+        )
+
+    # For `bazel run :pkg`.
+    _fuchsia_package_default_task(
+        name = name,
+        help_task = help_task,
+        debug_symbols_task = debug_symbols_task,
+        publish_task = anonymous_publish_task,
+        publish_cleanup_task = verbs.delete_repo(anonymous_publish_task),
+        component_run_tasks = component_run_tasks,
+        is_test = is_test,
+        package = package,
+        apply_fuchsia_transition = True,
+        tags = top_level_tags,
+        **kwargs
+    )
diff --git a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_publish.bzl b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_publish.bzl
index 4757c47..fc5eedb 100644
--- a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_publish.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_publish.bzl
@@ -5,9 +5,9 @@
 """Publishes packages as a workflow task."""
 
 load("@rules_fuchsia//fuchsia/private:providers.bzl", "FuchsiaPackageGroupInfo", "FuchsiaPackageInfo")
-load(":fuchsia_shell_task.bzl", "shell_task_rule")
+load(":fuchsia_task.bzl", "fuchsia_task_rule")
 
-def _fuchsia_task_publish_impl(ctx, make_shell_task):
+def _fuchsia_task_publish_impl(ctx, make_fuchsia_task):
     sdk = ctx.toolchains["@rules_fuchsia//fuchsia:toolchain"]
     far_files = [
         pkg.far_file
@@ -15,11 +15,13 @@
         for pkg in (dep[FuchsiaPackageGroupInfo].packages if FuchsiaPackageGroupInfo in dep else [dep[FuchsiaPackageInfo]])
     ]
 
-    repo_name_args = ["--repo_name", ctx.attr.package_repository_name] if ctx.attr.package_repository_name else []
-
-    return make_shell_task(
-        command = [
-            ctx.attr._publish_packages_tool,
+    repo_name_args = [
+        "--repo_name",
+        ctx.attr.package_repository_name,
+    ] if ctx.attr.package_repository_name else []
+    return make_fuchsia_task(
+        task_runner = ctx.attr._publish_packages_tool,
+        prepend_args = [
             "--ffx",
             sdk.ffx,
             "--pm",
@@ -33,7 +35,7 @@
     _fuchsia_task_publish,
     _fuchsia_task_publish_for_test,
     fuchsia_task_publish,
-) = shell_task_rule(
+) = fuchsia_task_rule(
     implementation = _fuchsia_task_publish_impl,
     doc = """A workflow task that publishes multiple fuchsia packages.""",
     toolchains = ["@rules_fuchsia//fuchsia:toolchain"],
@@ -43,7 +45,7 @@
             providers = [[FuchsiaPackageInfo], [FuchsiaPackageGroupInfo]],
         ),
         "package_repository_name": attr.string(
-            doc = "Repository name to publish these packages to",
+            doc = "Optionally specify the repository name to publish these packages to.",
         ),
         "_publish_packages_tool": attr.label(
             doc = "The publish_packages tool.",
diff --git a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_run_component.bzl b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_run_component.bzl
index 77f3ef7..b075d66 100644
--- a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_run_component.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_run_component.bzl
@@ -2,28 +2,89 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-"""ffx component run invokation as a workflow task."""
+"""Runs components, tests components, or register drivers within a package."""
 
-load(":fuchsia_task_ffx.bzl", "fuchsia_task_ffx")
+load(":fuchsia_task.bzl", "fuchsia_task_rule")
+load(":providers.bzl", "FuchsiaComponentInfo", "FuchsiaPackageInfo")
 
-def fuchsia_task_run_component(
-        *,
-        name,
-        # TODO(fxbug.dev/113205): Generate a default moniker.
-        moniker,
-        component_url,
-        arguments = [],
-        recreate = True,
-        **kwargs):
-    fuchsia_task_ffx(
-        name = name,
-        arguments = [
-            "component",
-            "run",
-            moniker,
-            component_url,
-        ] + (
-            ["--recreate"] if recreate else []
-        ) + arguments,
-        **kwargs
-    )
+def _fuchsia_task_run_component_impl(ctx, make_fuchsia_task):
+    sdk = ctx.toolchains["@rules_fuchsia//fuchsia:toolchain"]
+    repo = ctx.attr.repository
+    package = ctx.attr.package[FuchsiaPackageInfo].package_name
+    component = ctx.attr.component[FuchsiaComponentInfo]
+    component_name = component.name
+    manifest = component.manifest.basename
+    url = "fuchsia-pkg://%s/%s#meta/%s" % (repo, package, manifest)
+    moniker = ctx.attr.moniker or "/core/ffx-laboratory:%s" % component_name
+    if component.is_driver:
+        return make_fuchsia_task(
+            ctx.attr._register_driver_tool,
+            [
+                "--ffx",
+                sdk.ffx,
+                "--url",
+                url,
+            ],
+        )
+    elif component.is_test:
+        return make_fuchsia_task(
+            ctx.attr._run_test_component_tool,
+            [
+                "--ffx",
+                sdk.ffx,
+                "--url",
+                url,
+            ],
+        )
+    else:
+        return make_fuchsia_task(
+            ctx.attr._run_component_tool,
+            [
+                "--ffx",
+                sdk.ffx,
+                "--moniker",
+                moniker,
+                "--url",
+                url,
+            ],
+        )
+
+(
+    _fuchsia_task_run_component,
+    _fuchsia_task_run_component_for_test,
+    fuchsia_task_run_component,
+) = fuchsia_task_rule(
+    implementation = _fuchsia_task_run_component_impl,
+    toolchains = ["@rules_fuchsia//fuchsia:toolchain"],
+    attrs = {
+        "repository": attr.string(
+            doc = "The repository that has the published package.",
+            mandatory = True,
+        ),
+        "package": attr.label(
+            doc = "The package containing the component.",
+            providers = [FuchsiaPackageInfo],
+            mandatory = True,
+        ),
+        "moniker": attr.string(
+            doc = "The moniker to run the component in. Only used for non-test non-driver components.",
+        ),
+        "component": attr.label(
+            doc = "The component to run.",
+            providers = [FuchsiaComponentInfo],
+            mandatory = True,
+        ),
+        "_register_driver_tool": attr.label(
+            doc = "The tool used to run components",
+            default = "//fuchsia/tools:register_driver",
+        ),
+        "_run_test_component_tool": attr.label(
+            doc = "The tool used to run components",
+            default = "//fuchsia/tools:run_test_component",
+        ),
+        "_run_component_tool": attr.label(
+            doc = "The tool used to run components",
+            default = "//fuchsia/tools:run_component",
+        ),
+    },
+)
diff --git a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_run_driver_tool.bzl b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_run_driver_tool.bzl
new file mode 100644
index 0000000..7495e2b
--- /dev/null
+++ b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_run_driver_tool.bzl
@@ -0,0 +1,43 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Runs driver tools delivered within a package."""
+
+load(":fuchsia_task_ffx.bzl", "ffx_task_rule")
+load(":providers.bzl", "FuchsiaDriverToolInfo", "FuchsiaPackageInfo")
+
+def _fuchsia_task_run_driver_tool_impl(ctx, make_ffx_task):
+    repo = ctx.attr.repository
+    package = ctx.attr.package[FuchsiaPackageInfo].package_name
+    tool_binary = ctx.attr.tool[FuchsiaDriverToolInfo].binary.dest
+    url = "fuchsia-pkg://%s/%s#%s" % (repo, package, tool_binary)
+    return make_ffx_task(prepend_args = [
+        "driver",
+        "run-tool",
+        url,
+    ])
+
+(
+    _fuchsia_task_run_driver_tool,
+    _fuchsia_task_run_driver_tool_for_test,
+    fuchsia_task_run_driver_tool,
+) = ffx_task_rule(
+    implementation = _fuchsia_task_run_driver_tool_impl,
+    attrs = {
+        "repository": attr.string(
+            doc = "The repository that has the published package.",
+            mandatory = True,
+        ),
+        "package": attr.label(
+            doc = "The package containing the driver tool.",
+            providers = [FuchsiaPackageInfo],
+            mandatory = True,
+        ),
+        "tool": attr.label(
+            doc = "The driver tool to run.",
+            providers = [FuchsiaDriverToolInfo],
+            mandatory = True,
+        ),
+    },
+)
diff --git a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_verbs.bzl b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_verbs.bzl
index de90363..602c6f4 100644
--- a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_verbs.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_task_verbs.bzl
@@ -2,37 +2,56 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-def _make_verb(verb):
+def _make_verb(verb = None):
     def _make(name):
-        return name + "." + verb
+        return name + "." + verb if verb else name
 
     return _make
 
-def make_help_executable(ctx, verbs):
-    exe = ctx.actions.declare_file(ctx.label.name + "_help_text.sh")
-    tasks = ['echo "  - {}: {}"'.format(verb(ctx.label.name), help) for (verb, help) in verbs.items()]
+def make_help_executable(ctx, verbs, name = None):
+    name = name or ctx.label.name
+    exe = ctx.actions.declare_file(name + "_help_text.sh")
+    tasks = ['echo "  - {}: {}"'.format(verb(name), help) for (verb, help) in verbs.items()]
     ctx.actions.write(
         exe,
         """
-    echo "------------------------------------------------------"
-    echo "ERROR: The target {name} cannot be run directly."
-    echo "To interact with this object use the following tasks:"
+    echo "------------------------------------------------------"{default_target_invalid_str}
+    echo "USAGE: To interact with this object use the following tasks:"
     {tasks}
     echo "------------------------------------------------------"
-    """.format(name = ctx.label.name, tasks = "\n".join(tasks)),
+    """.format(
+            default_target_invalid_str = "" if _verbs.noverb in verbs else """
+echo "ERROR: The target '%s' cannot be run directly." """ % name,
+            tasks = "\n".join(tasks),
+        ),
         is_executable = True,
     )
     return exe
 
-verbs = struct(
-    create = _make_verb("create"),
-    delete = _make_verb("delete"),
-    delete_repo = _make_verb("delete_repo"),
-    fetch = _make_verb("fetch"),
-    make_default = _make_verb("make_default"),
-    reboot = _make_verb("reboot"),
-    remove = _make_verb("remove"),
-    start = _make_verb("start"),
-    stop = _make_verb("stop"),
-    wait = _make_verb("wait"),
-)
+def _make_verbs(*verbs):
+    return struct(
+        noverb = _make_verb(),
+        custom = _make_verb,
+        **{
+            verb: _make_verb(verb)
+            for verb in verbs
+        }
+    )
+
+_verbs = _make_verbs(*"""
+create
+debug_symbols
+delete
+delete_repo
+fetch
+help
+make_default
+publish
+reboot
+remove
+start
+stop
+wait
+""".strip().split("\n"))
+
+verbs = _verbs
diff --git a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_workflow.bzl b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_workflow.bzl
index 87c23b2..8c0825f 100644
--- a/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_workflow.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/workflows/fuchsia_workflow.bzl
@@ -27,23 +27,37 @@
                 initial[k] = v
     return initial
 
-def _fuchsia_workflow_impl(ctx, make_workflow, collect_arguments):
-    workflow_args, runfiles = collect_arguments()
-    return make_workflow(
-        FuchsiaWorkflowInfo(
-            entities = _collect_entities({
-                ctx.label: FuchsiaWorkflowEntityInfo(
-                    sequence = [dep[FuchsiaWorkflowInfo].entrypoint for dep in ctx.attr.sequence],
-                    args = workflow_args,
+def fuchsia_workflow_rule(*, implementation, **kwargs):
+    """Starlark higher-order rule for specifying a sequence of workflow entities."""
+
+    def _fuchsia_workflow_impl(ctx, make_workflow_entity, collect_arguments):
+        def _make_workflow(sequence, prepend_args = [], runfiles = []):
+            workflow_args, runfiles = collect_arguments(prepend_args, runfiles)
+            return make_workflow_entity(
+                FuchsiaWorkflowInfo(
+                    entities = _collect_entities({
+                        ctx.label: FuchsiaWorkflowEntityInfo(
+                            sequence = [dep[FuchsiaWorkflowInfo].entrypoint for dep in sequence],
+                            args = workflow_args,
+                        ),
+                    }, [dep[FuchsiaWorkflowInfo].entities for dep in sequence]),
+                    entrypoint = ctx.label,
                 ),
-            }, [dep[FuchsiaWorkflowInfo].entities for dep in ctx.attr.sequence]),
-            entrypoint = ctx.label,
-        ),
-        ctx.attr.sequence,
-        runfiles,
+                sequence,
+                runfiles,
+            )
+
+        return implementation(ctx, _make_workflow)
+
+    return workflow_entity_rule(
+        implementation = _fuchsia_workflow_impl,
+        **kwargs
     )
 
-_fuchsia_workflow, _fuchsia_workflow_for_test, fuchsia_workflow = workflow_entity_rule(
+def _fuchsia_workflow_impl(ctx, make_workflow):
+    return make_workflow(ctx.attr.sequence)
+
+_fuchsia_workflow, _fuchsia_workflow_for_test, fuchsia_workflow = fuchsia_workflow_rule(
     implementation = _fuchsia_workflow_impl,
     doc = """A grouping of tasks to be run sequentially.""",
     attrs = {
diff --git a/bazel_rules_fuchsia/fuchsia/private/workflows/providers.bzl b/bazel_rules_fuchsia/fuchsia/private/workflows/providers.bzl
index 6553024..e058815 100644
--- a/bazel_rules_fuchsia/fuchsia/private/workflows/providers.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/workflows/providers.bzl
@@ -6,7 +6,10 @@
 
 load(
     "@rules_fuchsia//fuchsia/private:providers.bzl",
+    _FuchsiaComponentInfo = "FuchsiaComponentInfo",
     _FuchsiaDebugSymbolInfo = "FuchsiaDebugSymbolInfo",
+    _FuchsiaDriverToolInfo = "FuchsiaDriverToolInfo",
+    _FuchsiaPackageInfo = "FuchsiaPackageInfo",
     _FuchsiaProductBundleInfo = "FuchsiaProductBundleInfo",
     _FuchsiaProvidersInfo = "FuchsiaProvidersInfo",
 )
@@ -36,6 +39,9 @@
     },
 )
 
+FuchsiaComponentInfo = _FuchsiaComponentInfo
 FuchsiaDebugSymbolInfo = _FuchsiaDebugSymbolInfo
+FuchsiaDriverToolInfo = _FuchsiaDriverToolInfo
+FuchsiaPackageInfo = _FuchsiaPackageInfo
 FuchsiaProductBundleInfo = _FuchsiaProductBundleInfo
 FuchsiaProvidersInfo = _FuchsiaProvidersInfo
diff --git a/bazel_rules_fuchsia/fuchsia/private/workflows/utils.bzl b/bazel_rules_fuchsia/fuchsia/private/workflows/utils.bzl
index e5f6185..44d83d6 100644
--- a/bazel_rules_fuchsia/fuchsia/private/workflows/utils.bzl
+++ b/bazel_rules_fuchsia/fuchsia/private/workflows/utils.bzl
@@ -10,6 +10,8 @@
     _alias = "alias",
     _collect_runfiles = "collect_runfiles",
     _flatten = "flatten",
+    _label_name = "label_name",
+    _normalized_target_name = "normalized_target_name",
     _rule_variants = "rule_variants",
     _wrap_executable = "wrap_executable",
 )
@@ -17,6 +19,8 @@
 alias = _alias
 collect_runfiles = _collect_runfiles
 flatten = _flatten
+label_name = _label_name
+normalized_target_name = _normalized_target_name
 rule_variants = _rule_variants
 with_fuchsia_transition = _with_fuchsia_transition
 wrap_executable = _wrap_executable
diff --git a/bazel_rules_fuchsia/fuchsia/tools/BUILD.bazel b/bazel_rules_fuchsia/fuchsia/tools/BUILD.bazel
index 5e9e515..42de923 100644
--- a/bazel_rules_fuchsia/fuchsia/tools/BUILD.bazel
+++ b/bazel_rules_fuchsia/fuchsia/tools/BUILD.bazel
@@ -69,15 +69,34 @@
     name = "fuchsia_shell_task",
     srcs = ["fuchsia_shell_task.py"],
     python_version = "PY3",
-    deps = [
-        ":fuchsia_task_lib",
-    ],
+    deps = [":fuchsia_task_lib"],
 )
 
 py_binary(
     name = "register_debug_symbols",
     srcs = ["register_debug_symbols.py"],
     python_version = "PY3",
+    deps = [":fuchsia_task_lib"],
+)
+
+py_binary(
+    name = "register_driver",
+    srcs = ["register_driver.py"],
+    python_version = "PY3",
+    deps = [":fuchsia_task_lib"],
+)
+
+py_binary(
+    name = "run_test_component",
+    srcs = ["run_test_component.py"],
+    python_version = "PY3",
+    deps = [":fuchsia_task_lib"],
+)
+
+py_binary(
+    name = "run_component",
+    srcs = ["run_component.py"],
+    python_version = "PY3",
 )
 
 py_binary(
diff --git a/bazel_rules_fuchsia/fuchsia/tools/fuchsia_task_lib.py b/bazel_rules_fuchsia/fuchsia/tools/fuchsia_task_lib.py
index 2d69507..2209a88 100644
--- a/bazel_rules_fuchsia/fuchsia/tools/fuchsia_task_lib.py
+++ b/bazel_rules_fuchsia/fuchsia/tools/fuchsia_task_lib.py
@@ -48,37 +48,6 @@
             # so don't apply the style.
             return text
 
-
-class TaskResult:
-    def __init__(self, property: str, value: Any, is_environment_variable: bool = False) -> None:
-        self._property = property
-        self._value = value
-        self._is_environment_variable = is_environment_variable
-
-    @property
-    def value(self) -> Any:
-        return self._value
-
-    @property
-    def is_environment_variable(self) -> bool:
-        return self._is_environment_variable
-
-    @property
-    def property(self) -> str:
-        return self._property
-
-    @classmethod
-    def from_dict(cls, entity: Dict[str, Any]) -> 'TaskResult':
-        return TaskResult(**entity)
-
-    def to_dict(self) -> Dict[str, Any]:
-        return {
-            'property': self.property,
-            'value': self.value,
-            'is_environment_variable': self.is_environment_variable,
-        }
-
-
 @total_ordering
 class ArgumentScope(Enum):
     # Captures arguments that are passed directly to the current task:
@@ -185,7 +154,7 @@
         *,
         task_name: str,
         is_final_task: bool,
-        workflow_state: List[TaskResult],
+        workflow_state: Dict[str, Any],
     ) -> None:
         self._task_name = task_name
         self._is_final_task = is_final_task
@@ -195,11 +164,7 @@
     def apply_environment(self) -> None:
         original_environ = os.environ.copy()
         try:
-            os.environ.update({
-                result.property: result.value
-                for result in self.workflow_state
-                if result.is_environment_variable
-            })
+            os.environ.update(self.workflow_state['environment_variables'] or {})
             yield
         finally:
             os.environ.clear()
@@ -219,13 +184,9 @@
         return self._is_final_task
 
     @property
-    def workflow_state(self) -> List[TaskResult]:
+    def workflow_state(self) -> Dict[str, Any]:
         return self._workflow_state
 
-    @workflow_state.setter
-    def workflow_state(self, value) -> None:
-        self._workflow_state = value
-
     def get_task_arguments(self, scope: ArgumentScope) -> List[str]:
         return ScopedArgumentParser.get_arguments(scope)
 
@@ -262,14 +223,11 @@
         task = cls(
             task_name=workflow_args.workflow_task_name,
             is_final_task=workflow_args.workflow_final_task,
-            workflow_state=[
-                TaskResult.from_dict(result)
-                for result in (
-                    json.loads(workflow_args.workflow_previous_state.read_text())
-                    if workflow_args.workflow_previous_state
-                    else []
-                )
-            ],
+            workflow_state=(
+                json.loads(workflow_args.workflow_previous_state.read_text())
+                if workflow_args.workflow_previous_state
+                else {'environment_variables': {}}
+            ),
         )
         try:
             with task.apply_environment():
@@ -280,9 +238,7 @@
         except KeyboardInterrupt:
             sys.exit(1)
         if workflow_args.workflow_next_state:
-            workflow_args.workflow_next_state.write_text(
-                json.dumps([result.to_dict() for result in task.workflow_state])
-            )
+            workflow_args.workflow_next_state.write_text(json.dumps(task.workflow_state))
 
 if __name__ == '__main__':
     FuchsiaTask.main()
diff --git a/bazel_rules_fuchsia/fuchsia/tools/publish_packages.py b/bazel_rules_fuchsia/fuchsia/tools/publish_packages.py
index c951680..225eca7 100644
--- a/bazel_rules_fuchsia/fuchsia/tools/publish_packages.py
+++ b/bazel_rules_fuchsia/fuchsia/tools/publish_packages.py
@@ -11,6 +11,8 @@
 from pathlib import Path
 from shutil import rmtree
 
+from fuchsia_task_lib import *
+
 def run(*command):
     try:
         return subprocess.check_output(
@@ -19,216 +21,205 @@
         ).strip()
     except subprocess.CalledProcessError as e:
         print(e.stdout)
-        raise e
+        raise TaskExecutionException(f'Command {command} failed.')
 
-def parse_args():
-    '''Parses arguments.'''
-    parser = argparse.ArgumentParser()
+class FuchsiaTaskPublish(FuchsiaTask):
 
-    def path_arg(type='file'):
-        def arg(path):
-            path = Path(path)
-            if path.is_file() != (type == 'file') or path.is_dir() != (type == 'directory'):
-                parser.error(f'Path "{path}" is not a {type}!')
-            return path
-        return arg
+    def parse_args(self, parser: ScopedArgumentParser) -> argparse.Namespace:
+        '''Parses arguments.'''
 
-    parser.add_argument(
-        '--ffx',
-        type=path_arg(),
-        help='A path to the ffx tool.',
-        required=True,
-    )
-    parser.add_argument(
-        '--pm',
-        type=path_arg(),
-        help='A path to the pm tool.',
-        required=True,
-    )
-    parser.add_argument(
-        '--packages',
-        help='Paths to far files to package.',
-        nargs='+',
-        type=path_arg(),
-        required=True,
-    )
-    parser.add_argument(
-        '--repo_name',
-        help='Optionally specify the repository name.',
-    )
-    parser.add_argument(
-        '--target',
-        help='Optionally specify the target fuchsia device.',
-        required=False,
-    )
+        parser.add_argument(
+            '--ffx',
+            type=parser.path_arg(),
+            help='A path to the ffx tool.',
+            required=True,
+        )
+        parser.add_argument(
+            '--pm',
+            type=parser.path_arg(),
+            help='A path to the pm tool.',
+            required=True,
+        )
+        parser.add_argument(
+            '--packages',
+            help='Paths to far files to package.',
+            nargs='+',
+            type=parser.path_arg(),
+            required=True,
+        )
+        parser.add_argument(
+            '--repo_name',
+            help='Optionally specify the repository name.',
+            scope=ArgumentScope.GLOBAL,
+        )
+        parser.add_argument(
+            '--target',
+            help='Optionally specify the target fuchsia device.',
+            required=False,
+            scope=ArgumentScope.GLOBAL,
+        )
 
-    # Private arguments.
-    # Whether to make the repo default.
-    parser.add_argument(
-        '--prompt_make_repo_default',
-        help=argparse.SUPPRESS,
-        required=False
-    )
-    # Whether to make the repo default.
-    parser.add_argument(
-        '--prompt_repo_cleanup',
-        help=argparse.SUPPRESS,
-        required=False
-    )
-    # The effective repo path to publish to.
-    parser.add_argument(
-        '--repo_path',
-        help=argparse.SUPPRESS,
-        required=False,
-    )
-    return parser.parse_args()
+        # Private arguments.
+        # Whether to make the repo default.
+        parser.add_argument(
+            '--make_repo_default',
+            action='store_true',
+            help=argparse.SUPPRESS,
+            required=False
+        )
+        # The effective repo path to publish to.
+        parser.add_argument(
+            '--repo_path',
+            help=argparse.SUPPRESS,
+            required=False,
+        )
+        return parser.parse_args()
 
-def ensure_target_device(args):
-    args.target = args.target or run(args.ffx, 'target', 'default', 'get')
-    print(f'Waiting for {args.target} to come online (60s)')
-    run(args.ffx, '--target', args.target, 'target', 'wait', '-t', '60')
+    def enable_ffx_repository(self, args):
+        if run(args.ffx, 'config', 'get', 'repository.server.enabled') != 'true':
+            print('The ffx repository server is not enabled, starting it now...')
+            run(args.ffx, 'repository', 'server', 'start')
 
-def resolve_repo(args):
-    # Determine the repo name we want to use, in this order:
-    # 1. User specified argument.
-    def user_specified_repo_name():
-        if args.repo_name:
-            print(f'Using manually specified --repo_name: {args.repo_name}')
-            # We shouldn't ask to delete a user specified --repo_name.
-            if args.prompt_repo_cleanup is None:
-                args.prompt_repo_cleanup = False
-        return args.repo_name
-    # 2. ffx default repository.
-    def ffx_default_repo():
-        default = run(args.ffx, 'repository', 'default', 'get')
-        if default:
-            print(f'Using ffx default repository: {default}')
-            # We shouldn't ask to delete the ffx default repo.
-            if args.prompt_repo_cleanup is None:
-                args.prompt_repo_cleanup = False
-        return default
-    # 3. A user prompt.
-    def prompt_repo():
-        print('--repo_name was not specified and there is no default ffx repository set.')
-        repo_name = input('Please specify a repo name to publish to: ')
-        if args.prompt_make_repo_default is None:
-            args.prompt_make_repo_default = input('Would you make this repo your default ffx repo? (y/n): ').lower() == 'y'
-        # We shouldn't ask to delete a repo that we're going to make default.
-        if args.prompt_repo_cleanup is None:
-            args.prompt_repo_cleanup = not args.prompt_make_repo_default
-        return repo_name
-    args.repo_name = user_specified_repo_name() or ffx_default_repo() or prompt_repo()
+    def ensure_target_device(self, args):
+        args.target = args.target or run(args.ffx, 'target', 'default', 'get')
+        print(f'Waiting for {args.target} to come online (60s)')
+        run(args.ffx, '--target', args.target, 'target', 'wait', '-t', '60')
 
-    # Determine the pm repo path (use the existing one from ffx, or create a new one).
-    existing_repos = json.loads(run(
-        args.ffx,
-        '--machine',
-        'json',
-        'repository',
-        'list',
-    ))
-    existing_repo = ([
-        repo
-        for repo in existing_repos
-        if repo['name'] == args.repo_name
-    ] or [None])[0]
-    existing_repo_path = existing_repo and Path(
-        existing_repo['spec']['path']
-    )
-    args.repo_path = existing_repo_path or Path(tempfile.mkdtemp())
+    def resolve_repo(self, args):
+        # Determine the repo name we want to use, in this order:
+        # 1. User specified argument.
+        def user_specified_repo_name():
+            if args.repo_name:
+                print(f'Using manually specified --repo_name: {args.repo_name}')
+                # We shouldn't ask to delete a user specified --repo_name.
+                self.prompt_repo_cleanup = False
+            return args.repo_name
+        # 2. ffx default repository.
+        def ffx_default_repo():
+            default = run(args.ffx, '-c', 'ffx_repository=true', 'repository', 'default', 'get')
+            if default:
+                print(f'Using ffx default repository: {default}')
+                # We shouldn't ask to delete the ffx default repo.
+                self.prompt_repo_cleanup = False
+            return default
+        # 3. A user prompt.
+        def prompt_repo():
+            print('--repo_name was not specified and there is no default ffx repository set.')
+            repo_name = input('Please specify a repo name to publish to: ')
+            if args.make_repo_default is None:
+                args.make_repo_default = input('Would you make this repo your default ffx repo? (y/n): ').lower() == 'y'
+            # We shouldn't ask to delete a repo that we're going to make default.
+            self.prompt_repo_cleanup = not args.make_repo_default
+            return repo_name
+        args.repo_name = user_specified_repo_name() or ffx_default_repo() or prompt_repo()
 
-def ensure_repo(args):
-    # Ensure the ffx repository server.
-    if run(args.ffx, 'config', 'get', 'repository.server.enabled') != 'true':
-        print('The ffx repository server is not enabled, starting it now...')
-        run(args.ffx, 'repository', 'server', 'start')
+        # Determine the pm repo path (use the existing one from ffx, or create a new one).
+        existing_repos = json.loads(run(
+            args.ffx,
+            '--machine',
+            'json',
+            'repository',
+            'list',
+        ))
+        existing_repo = ([
+            repo
+            for repo in existing_repos
+            if repo['name'] == args.repo_name
+        ] or [None])[0]
+        existing_repo_path = existing_repo and Path(
+            existing_repo['spec']['path']
+        )
+        args.repo_path = existing_repo_path or Path(tempfile.mkdtemp())
 
-    # Ensure pm repo.
-    if (args.repo_path / 'repository').is_dir():
-        print(f'Using existing pm repo: {args.repo_path}')
-    else:
-        print(f'Creating a new pm repository: {args.repo_path}')
-        run(args.pm, 'newrepo', '-vt', '-repo', args.repo_path)
+    def ensure_repo(self, args):
+        # Ensure pm repo.
+        if (args.repo_path / 'repository').is_dir():
+            print(f'Using existing pm repo: {args.repo_path}')
+        else:
+            print(f'Creating a new pm repository: {args.repo_path}')
+            run(args.pm, 'newrepo', '-vt', '-repo', args.repo_path)
 
-    # Ensure ffx repository.
-    print(f'Associating {args.repo_name} to {args.repo_path}')
-    run(
-        args.ffx,
-        'repository',
-        'add-from-pm',
-        '--repository',
-        args.repo_name,
-        args.repo_path,
-    )
-
-    # Ensure ffx target repository.
-    print(f'Registering {args.repo_name} to target device {args.target}')
-    run(
-        args.ffx,
-        '--target',
-        args.target,
-        'target',
-        'repository',
-        'register',
-        '-r',
-        args.repo_name,
-    )
-
-    # Optionally make the ffx repository default.
-    if args.prompt_make_repo_default:
-        old_default = run(args.ffx, 'repository', 'default', 'get')
-        print(f'Setting default ffx repository "{old_default}" => "{args.repo_name}"')
+        # Ensure ffx repository.
+        print(f'Associating {args.repo_name} to {args.repo_path}')
         run(
             args.ffx,
             'repository',
-            'default',
-            'set',
+            'add-from-pm',
+            '--repository',
+            args.repo_name,
+            args.repo_path,
+        )
+
+        # Ensure ffx target repository.
+        print(f'Registering {args.repo_name} to target device {args.target}')
+        run(
+            args.ffx,
+            '--target',
+            args.target,
+            'target',
+            'repository',
+            'register',
+            '-r',
             args.repo_name,
         )
 
-def publish_packages(args):
-    # TODO(fxbug.dev/110617): Publish all packages with 1 command invocation.
-    print(f'Publishing packages: {args.packages}')
-    for package in args.packages:
-        run(
-            args.pm,
-            'publish',
-            '-vt',
-            '-a',
-            '-f',
-            package,
-            '-repo',
-            args.repo_path,
-        )
-    print(f'Published {len(args.packages)} packages')
+        # Optionally make the ffx repository default.
+        if args.make_repo_default:
+            old_default = run(args.ffx, 'repository', 'default', 'get')
+            print(f'Setting default ffx repository "{old_default}" => "{args.repo_name}"')
+            run(
+                args.ffx,
+                'repository',
+                'default',
+                'set',
+                args.repo_name,
+            )
 
-def teardown(args):
-    if args.prompt_repo_cleanup != False:
-        input('Press enter to delete this repository, or ^C to quit.')
+    def publish_packages(self, args):
+        # TODO(fxbug.dev/110617): Publish all packages with 1 command invocation.
+        print(f'Publishing packages: {args.packages}')
+        for package in args.packages:
+            run(
+                args.pm,
+                'publish',
+                '-vt',
+                '-a',
+                '-f',
+                package,
+                '-repo',
+                args.repo_path,
+            )
+        print(f'Published {len(args.packages)} packages')
 
-        # Delete the pm repository.
-        rmtree(args.repo_path)
-        print(f'Deleted {args.repo_path}')
+    def teardown(self, args):
+        if self.prompt_repo_cleanup:
+            input('Press enter to delete this repository, or ^C to quit.')
 
-        # Remove the ffx repository.
-        run('ffx', 'repository', 'remove', args.repo_name)
-        print(f'Removed the ffx repository {args.repo_name}')
+            # Delete the pm repository.
+            rmtree(args.repo_path)
+            print(f'Deleted {args.repo_path}')
 
+            # Remove the ffx repository.
+            run('ffx', 'repository', 'remove', args.repo_name)
+            print(f'Removed the ffx repository {args.repo_name}')
 
-def main():
-    # Parse arguments.
-    args = parse_args()
+    def run(self, parser: ScopedArgumentParser) -> None:
+        # Parse arguments.
+        args = self.parse_args(parser)
 
-    # Check environment and gather information for publishing.
-    ensure_target_device(args)
-    resolve_repo(args)
+        # Check environment and gather information for publishing.
+        self.enable_ffx_repository(args)
+        self.ensure_target_device(args)
+        self.resolve_repo(args)
 
-    # Perform the publishing.
-    ensure_repo(args)
-    publish_packages(args)
+        # Perform the publishing.
+        self.ensure_repo(args)
+        self.publish_packages(args)
 
-    # Optionally cleanup the repo.
-    teardown(args)
+        self.workflow_state['environment_variables']['FUCHSIA_REPO_NAME'] = args.repo_name
+
+        # Optionally cleanup the repo.
+        self.teardown(args)
 
 if __name__ == '__main__':
-    main()
+    FuchsiaTaskPublish.main()
diff --git a/bazel_rules_fuchsia/fuchsia/tools/register_driver.py b/bazel_rules_fuchsia/fuchsia/tools/register_driver.py
new file mode 100644
index 0000000..c5254a3
--- /dev/null
+++ b/bazel_rules_fuchsia/fuchsia/tools/register_driver.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+import subprocess
+
+from fuchsia_task_lib import *
+
+
+class FuchsiaTaskRegisterDriver(FuchsiaTask):
+    def parse_args(self, parser: ScopedArgumentParser) -> argparse.Namespace:
+        '''Parses arguments.'''
+
+        parser.add_argument(
+            '--ffx',
+            type=parser.path_arg(),
+            help='A path to the ffx tool.',
+            required=True,
+        )
+        parser.add_argument(
+            '--url',
+            type=str,
+            help='The full component url.',
+            required=True,
+        )
+        parser.add_argument(
+            '--target',
+            help='Optionally specify the target fuchsia device.',
+            required=False,
+            scope=ArgumentScope.GLOBAL,
+        )
+        return parser.parse_args()
+
+
+    def run(self, parser: ScopedArgumentParser) -> None:
+        args = self.parse_args(parser)
+        ffx = [args.ffx] + (['--target', args.target] if args.target else [])
+
+        subprocess.check_call([
+            *ffx,
+            'driver',
+            'register',
+            args.url,
+        ])
+
+
+if __name__ == '__main__':
+    FuchsiaTaskRegisterDriver.main()
diff --git a/bazel_rules_fuchsia/fuchsia/tools/run_component.py b/bazel_rules_fuchsia/fuchsia/tools/run_component.py
new file mode 100644
index 0000000..f1062f9
--- /dev/null
+++ b/bazel_rules_fuchsia/fuchsia/tools/run_component.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+import subprocess
+
+from fuchsia_task_lib import *
+
+
+class FuchsiaTaskRunComponent(FuchsiaTask):
+    def parse_args(self, parser: ScopedArgumentParser) -> argparse.Namespace:
+        '''Parses arguments.'''
+
+        parser.add_argument(
+            '--ffx',
+            type=parser.path_arg(),
+            help='A path to the ffx tool.',
+            required=True,
+        )
+        parser.add_argument(
+            '--moniker',
+            type=str,
+            help='The moniker to add the component to.',
+            required=True,
+        )
+        parser.add_argument(
+            '--url',
+            type=str,
+            help='The full component url.',
+            required=True,
+        )
+        parser.add_argument(
+            '--session',
+            action='store_true',
+            help='Whether to add this component to the session.',
+            required=False,
+        )
+        parser.add_argument(
+            '--target',
+            help='Optionally specify the target fuchsia device.',
+            required=False,
+            scope=ArgumentScope.GLOBAL,
+        )
+        return parser.parse_args()
+
+
+    def run(self, parser: ScopedArgumentParser) -> None:
+        args = self.parse_args(parser)
+        ffx = [args.ffx] + (['--target', args.target] if args.target else [])
+
+        if args.session:
+            subprocess.check_call([
+                *ffx,
+                'session',
+                'add',
+                args.url,
+            ])
+        else:
+            subprocess.check_call([
+                *ffx,
+                'component',
+                'run',
+                args.moniker,
+                args.url,
+                '--recreate',
+            ])
+
+
+if __name__ == '__main__':
+    FuchsiaTaskRunComponent.main()
diff --git a/bazel_rules_fuchsia/fuchsia/tools/run_test_component.py b/bazel_rules_fuchsia/fuchsia/tools/run_test_component.py
new file mode 100644
index 0000000..675365b
--- /dev/null
+++ b/bazel_rules_fuchsia/fuchsia/tools/run_test_component.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+import subprocess
+
+from fuchsia_task_lib import *
+
+
+class FuchsiaTaskRunTestComponent(FuchsiaTask):
+    def parse_args(self, parser: ScopedArgumentParser) -> argparse.Namespace:
+        '''Parses arguments.'''
+
+        parser.add_argument(
+            '--ffx',
+            type=parser.path_arg(),
+            help='A path to the ffx tool.',
+            required=True,
+        )
+        parser.add_argument(
+            '--url',
+            type=str,
+            help='The full component url.',
+            required=True,
+        )
+        parser.add_argument(
+            '--target',
+            help='Optionally specify the target fuchsia device.',
+            required=False,
+            scope=ArgumentScope.GLOBAL,
+        )
+        return parser.parse_args()
+
+
+    def run(self, parser: ScopedArgumentParser) -> None:
+        args = self.parse_args(parser)
+        ffx = [args.ffx] + (['--target', args.target] if args.target else [])
+
+        subprocess.check_call([
+            *ffx,
+            'test',
+            'run',
+            args.url,
+        ])
+
+
+if __name__ == '__main__':
+    FuchsiaTaskRunTestComponent.main()
diff --git a/tests/examples/hello_world_cpp/BUILD.bazel b/tests/examples/hello_world_cpp/BUILD.bazel
index 87c35f6..51ec84a 100644
--- a/tests/examples/hello_world_cpp/BUILD.bazel
+++ b/tests/examples/hello_world_cpp/BUILD.bazel
@@ -9,7 +9,7 @@
     "fuchsia_component",
     "fuchsia_component_manifest",
     "fuchsia_package",
-    "fuchsia_test_package",
+    "fuchsia_unittest_package",
     "if_fuchsia",
 )
 
@@ -63,10 +63,10 @@
     ]),
 )
 
-fuchsia_test_package(
+fuchsia_unittest_package(
     name = "test_pkg",
     package_name = "hello_tests",
-    test_components = [
+    unit_tests = [
         ":hello_test",
     ],
     visibility = ["//visibility:public"],