Developing an ffx plugin

This page describes the basic steps for creating a plugin for ffx.

The plugin system employs a combination of GN build rules and Rust attributes to decouple plugin code from ffx internals.

GN Build Rule

Use the ffx_plugin() build rule template in your project's BUILD.gn file to create a build target for your plugin.

Your BUILD.gn file should look similar to the following example:

import("//src/developer/ffx/build/ffx_plugin.gni")

ffx_plugin("ffx_example") {
  version = "0.1.0"
  edition = "2021"
  with_unit_tests = true
  deps = []
  args_sources = [
    "src/args.rs",
  ]
  sources = [
    "src/lib.rs",
  ]
}

Inside the src/ directory, the project should contain two source files:

  • src/args.rs: Defines the CLI parameters for your plugin.
  • src/lib.rs: Contains the main plugin source code implementation.

Arguments

Create the file src/args.rs containing the plugins supported arguments:

use {argh::FromArgs, ffx_core::ffx_command};

#[ffx_command()]
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "example", description = "an example")]
pub struct ExampleCommand {}

This uses the argh crate and more documentation can be found here. This struct has been decorated by the ffx_command attribute that signifies that your plugin should run when someone types the following command:

fx ffx example

If you want to add more parameters for your plugins, you add them to this struct.

An example parameter would look like this:

use {argh::FromArgs, ffx_core::ffx_command};

#[ffx_command()]
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "example", description = "an example")]
pub struct ExampleCommand {
    #[argh(positional)]
    /// example optional positional string parameter
    pub example: Option<String>,
}

See more documentation: - Argh

Plugin

Create the file src/lib.rs containing the plugin implementation:

use {
    anyhow::Result,
    ffx_core::ffx_plugin,
    ffx_example_args::ExampleCommand,
};

#[ffx_plugin()]
pub async fn example(_cmd: ExampleCommand) -> Result<()> {
    println!("Hello from the example plugin :)");
    Ok(())
}

Plugin methods need to accept the argh command created in the src/args.rs file as a parameter even if they do not use them.

Integration

Add your plugin library as a dependency to ffx to include it in the build. Edit the plugin_deps array in the ffx build target to add your ffx_plugin() target to the top level:

  plugin_deps = [
    "//path/to/your/plugin/dir:ffx_example",
    ...
  ]

To build and test your plugin, build ffx:

fx build ffx

You should now see the output when you run your example command:

$ fx ffx example
Hello from the example plugin :)

Unit tests

If you want to unit test your plugin, just follow the standard method for testing rust code on a host. The ffx_plugin() GN template generates a <target_name>_lib_test library target for unit tests when the with_unit_tests parameter is set to true.

If your lib.rs contains tests, they can be invoked using fx test:

fx test ffx_example_lib_test

If fx test doesn't find your test, check that the product configuration includes your test. You can include all the ffx tests with this command:

fx set ... --with=//src/developer/ffx:tests

FIDL protocols

FFX plugins can communicate with a target device using FIDL protocols through Overnet. To access FIDL protocols from your plugin, follow the instructions in this section.

  1. Add the FIDL Rust bindings as a dependency to the plugin's BUILD.gn file. The following example adds bindings for the fuchsia.device FIDL library:

    import("//src/developer/ffx/build/ffx_plugin.gni")
    
    ffx_plugin("ffx_example") {
      version = "0.1.0"
      edition = "2021"
      with_unit_tests = true
      deps = [
        "//sdk/fidl/fuchsia.device:fuchsia.device_rust",
      ]
      args_sources = [
        "src/args.rs",
      ]
      sources = [
        "src/lib.rs",
      ]
    }
    
  2. Import the necessary bindings int your plugin implementation. The following example imports NameProviderProxy from fuchsia.device:

    use {
        anyhow::Result,
        ffx_core::ffx_plugin,
        ffx_example_args::ExampleCommand,
        fidl_fuchsia_device::NameProviderProxy,
    };
    
  3. Include the FIDL proxy can be used in the plugin implementation. Plugins can accept proxies in the parameters list:

    pub async fn example(
        name_proxy: NameProviderProxy,
        _cmd: ExampleCommand,
    ) -> Result<()> { }
    
  4. Map the proxy type to a component selector representing the component providing the FIDL protocol in the ffx_plugin() annotation:

    #[ffx_plugin(
        NameProviderProxy = "bootstrap/device_name_provider:out:fuchsia.device.NameProvider"
    )]
    

The example plugin implementation in src/lib.rs should now look like the following:

use {
    anyhow::Result,
    ffx_core::ffx_plugin,
    ffx_example_args::ExampleCommand,
    fidl_fuchsia_device::NameProviderProxy,
};

#[ffx_plugin(
    NameProviderProxy = "bootstrap/device_name_provider:out:fuchsia.device.NameProvider"
)]
pub async fn example(
    name_proxy: NameProviderProxy,
    _cmd: ExampleCommand,
) -> Result<()> {
    if let Ok(name) = name_proxy.get_device_name().await? {
        println!("Hello, {}", name);
    }
    Ok(())
}

Repeat these steps to include additional FIDL proxies to your ffx plugin.

The following FIDL proxies are built into ffx, and do not require additional dependencies or mappings:

You can simply add the above proxies to your plugin's parameter list to access them in your implementation.

Proxy moniker map

ffx and the Remote Control Service (RCS) provide a mechanism for maintaining compatibility with existing monikers used by ffx plugins if the moniker representing a given FIDL proxy changes. For example:

  • The FIDL proxy is provided by a new component
  • The FIDL protocol name changes
  • The proxy moniker varies across product builds

RCS supports this using moniker maps that override the monikers defined in an ffx plugin's source and map it to a different value. To override a given moniker, add an entry to //src/developer/remote-control/data/moniker-map.json in the following format:

{
  ...
  "original/moniker:out:fuchsia.MyService": "some/new/moniker:expose:fuchsia.MyOtherService"
}

This example enables RCS to override references to original/moniker:out:fuchsia.MyService in ffx plugins and route them to some/new/moniker:expose:fuchsia.MyOtherService in any build which contains the mapping.