Migrating existing plugins to subtools

Why migrate

The new FHO subtool library provides some helpful macros for migrating an existing plugin over to the new subtool interface. By migrating your plugin to a subtool, you have these benefits:

  • A clearer interface boundary for errors.
  • Type-safe machine writer output and eventually schema validation.
  • More flexibility in what kinds of inputs your subtool takes from the fuchsia and ffx environment (see the FHO docs linked above).
  • Less macro magic.
  • Faster build times when built separately from ffx itself.

Please note that if you are starting a new subtool, you can skip a lot of the steps that were involved in the legacy plugin system, so you should follow the getting started instructions instead.

Non-recursive plugins

Plugins that don't have sub-plugins, with no plugin_deps section in their ffx_plugin() action, are relatively simple to migrate. Migrating plugins that have sub-plugins is treated as a separate task.

The following instructions are largely based on the differences between the ffx daemon echo plugin as originally written and the ffx echo subtool used as a proof of concept in developing the new subtool interface.

Migrating the rust lib.rs:

Given a plugin lib.rs file that looks like this:

use anyhow::Result;
use ffx_core::ffx_plugin;
use ffx_echo_args::EchoCommand;
use ffx_writer::Writer;
use fidl_fuchsia_developer_ffx::EchoProxy;

#[ffx_plugin(EchoProxy = "daemon::protocol")]
pub async fn echo(echo_proxy: EchoProxy, cmd: EchoCommand, #[ffx(machine = String)] mut writer: Writer) -> Result<()> {
    // implementation here
    Ok(())
}

The simplest way to migrate to the new plugin system is to remove the #[ffx_plugin] macro and add the following derive-macro-based code:

use fho::{FfxTool, FfxMain, MachineWriter, Result};
use ffx_echo_args::EchoCommand;
use fidl_fuchsia_developer_ffx::EchoProxy;

#[derive(FfxTool)]
pub struct EchoTool {
    #[command]
    cmd: EchoCommand,
    #[with(fho::daemon_protocol())]
    echo_proxy: ffx::EchoProxy,
}

#[async_trait(?Send)]
impl FfxMain for EchoTool {
    type Writer = MachineWriter<String>;
    async fn main(self, writer: MachineWriter<String>) -> Result<()> {
        // implementation here
        Ok(())
    }
}

Unlike the previous macro, there are no restrictions on naming the members of the EchoTool struct, and you can even implement your own loaders by deriving a trait from FHO if you want to do something more complicated.

The main() function is given this struct and the writer as moved objects, so you can feel free to deconstruct them however you like.

Also note that the Result type comes from FHO and not anyhow. At this boundary, there is now a specific error type that holds information about whether or not the error is user-surfaceable and how to present it. See errors for more information on how to work with this error type and its interactions with the ffx_error and ffx_bail macros.

For now, most migrated plugins still need to be included into the main ffx binary, and that means you'll need to also add the following macro invocation to generate the legacy plugin entry points:

fho::embedded_plugin!(EchoTool);

Adding a main.rs

Since your subtool can now be run independently, you will need to add a simple main.rs that will call into FHO and your plugin lib to properly run when ffx invokes it:

use ffx_tool_echo::EchoTool;
use fho::FfxTool;

#[fuchsia_async::run_singlethreaded]
async fn main() {
    EchoTool::execute_tool().await
}

In general, you won't ever have a main.rs any more complicated than this.

BUILD.gn for top level plugins

For existing plugins that still need to integrate with the existing ffx plugin system, these plugins will continue using the ffx_plugin GN action since it sets all the dependencies up properly for inclusion in the ffx binary.

If the plugin is a top level subcommand of ffx, then we'll also add an ffx_tool() action as well to build the separately compiled plugin:

# Existing import for the plugin action
import("//src/developer/ffx/build/ffx_plugin.gni")
# New import for the ffx_tool action.
import("//src/developer/ffx/build/ffx_tool.gni")

ffx_plugin("ffx_echo") {
  version = "0.1.0"
  edition = "2021"
  with_unit_tests = true

  args_sources = [ "src/args.rs" ]

  sources = [ "src/lib.rs" ]

  deps = [ "//src/developer/ffx/fidl:fuchsia.developer.ffx_rust" ]

  test_deps = [ "//src/lib/fuchsia-async" ]
}

# This will generate the executable file for your plugin.
ffx_tool("ffx_echo_tool") {
  edition = "2021"
  output_name = "ffx-echo"
  deps = [
    ":ffx_echo",
    "//src/developer/ffx/lib/fho:lib",
    "//src/lib/fuchsia-async",
  ]
  sources = [ "src/main.rs" ]

  # To be included in the sdk in the future, add this:
  sdk_category = "partner"
  sdk_target_name = "ffx_echo_tool_sdk"
}

Adding separate subtool building to the SDK

At this point, it will be possible to build the subtool but it won't be actively built. In order to do that, you'll need to add it to the list of "dual-mode plugins" in the ffx config.gni:

# ...snip...
dual_mode_plugins = [
  # ...
  "//path/to/your/plugin:ffx_echo_tool",
  # ...
]
# ...snip...

Now, running fx build ffx should cause both ffx and your plugin to be rebuilt, and if you run ffx commands you should be able to see your command in the "Workspace Commands" list.