开始开发 ffx 子工具

FFX 子工具是 ffx cli 可以运行的顶级命令。这些命令可以直接编译为 ffx 和/或构建为单独的命令(位于 build 输出目录或 SDK 中),然后系统会使用 FHO 工具接口调用这些命令。

本文档介绍了如何开始为 ffx 编写新的子工具。如果您已经有在采用新界面之前编写的插件并希望将其迁移到新的子工具接口,可以在迁移文档中找到更多信息。

放置位置

首先,在 fuchsia.git 树中的某个目录下创建一个用于存放子工具的目录。目前子工具位于以下位置:

  • ffx 插件树(仅限内置插件和混合插件/子工具所在的树)。通常不应在此处放置新的子工具。
  • ffx 工具树(仅限外部运行的子工具)。将它放入此处后,ffx 的维护人员可以更轻松地协助解决任何问题,或者通过对 ffx 与该工具之间的接口进行任何更改来更新您的子工具。如果您将其放在此处,而 FFX 团队不是此工具的主要维护人员,则必须OWNERS 文件放在您放置的目录中,以添加团队的组件和一些个人所有者,以便我们了解如何对工具问题进行分类。
  • 位于项目自身树中的某个位置。如果 ffx 工具只是现有程序的封装容器,这样做是合理的,但如果这样做,您必须设置 OWNERS 文件,以便 FFX 团队可以批准与 ffx 交互的部分的更新。为此,您可以将 file:/src/developer/ffx/OWNERS 添加到 OWNERS 文件(位于该工具所在的子目录中)。

除了不在插件中部署新工具之外,在确定特定位置时,可能需要与工具团队讨论才能确定最佳位置。

哪些文件

确定工具的目标位置后,请创建源文件。与旧版插件系统不同,无需将子工具拆分为三个单独的 Rust 库。不过,最佳做法是将工具的代码拆分为一个实现内容的库和一个仅调用该库的 main.rs

以下文件集是一个正常的起点:

BUILD.gn
src/lib.rs
src/main.rs
OWNERS

当然,您也可以根据需要将内容拆分到更多库中。请注意,这些示例都是基于 echo subtool 示例,为简洁起见,我们可能会移除或简化其中的部分内容。如果其中的任何内容不起作用或似乎不清楚,请查看该目录中的文件。

BUILD.gn

以下是一个简单的子工具的 BUILD.gn 文件的简单示例。请注意,如果您习惯使用旧版插件接口,则 ffx_tool 操作不会强制您采用库结构,也不会执行任何非常复杂的操作。这是一个相当简单的 rustc_binary 操作封装容器,但添加了一些用于生成元数据、生成主机工具和生成 SDK Atom 的额外目标。

# 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("//build/host.gni")
import("//build/rust/rustc_library.gni")
import("//src/developer/ffx/build/ffx_tool.gni")
import("//src/developer/ffx/lib/version/build/ffx_apply_version.gni")

rustc_library("lib") {
  # This is named as such to avoid a conflict with an existing ffx echo command.
  name = "ffx_tool_echo"
  edition = "2021"
  with_unit_tests = true

  deps = [
    "//src/developer/ffx/fidl:fuchsia.developer.ffx_rust",
    "//src/developer/ffx/lib/fho:lib",
    "//third_party/rust_crates:argh",
    "//third_party/rust_crates:async-trait",
  ]

  test_deps = [
    "//src/lib/fidl/rust/fidl",
    "//src/lib/fuchsia",
    "//src/lib/fuchsia-async",
    "//third_party/rust_crates:futures-lite",
  ]

  sources = [ "src/lib.rs" ]
}

ffx_tool("ffx_echo") {
  edition = "2021"
  output_name = "ffx-echo"
  deps = [
    ":lib",
    "//src/developer/ffx/lib/fho:lib",
    "//src/lib/fuchsia-async",
  ]
  sources = [ "src/main.rs" ]
}

group("echo") {
  public_deps = [
    ":ffx_echo",
    ":ffx_echo_host_tool",
  ]
}

group("bin") {
  public_deps = [ ":ffx_echo_versioned" ]
}

group("tests") {
  testonly = true
  deps = [ ":lib_test($host_toolchain)" ]
}

main.rs

主 Rust 文件通常相当简单,只需使用正确的类型调用 FHO 即可作为 ffx 知道如何与之通信的入口点:

// 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.
use ffx_tool_echo::EchoTool;
use fho::FfxTool;

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

lib.rs

这是工具的主要代码所在的位置。在这里,您要为命令参数设置一个基于参数的结构体,并从结构中派生 FfxToolFfxMain 实现,该结构可容纳您的工具需要运行的上下文。

参数

#[derive(ArgsInfo, FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "echo", description = "run echo test against the daemon")]
pub struct EchoCommand {
    #[argh(positional)]
    /// text string to echo back and forth
    pub text: Option<String>,
}

此结构体定义了子工具的子命令名称后面需要的任何参数。

工具结构

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

这个结构包含工具所需的上下文。这包括上面定义的参数结构、您可能需要的任何守护程序或设备的任何代理,或者您可以自行定义的其他内容。

此结构体中必须有一个引用上述参数类型的元素,并且该元素上应该具有 #[command] 属性,以便能够为 FfxTool 实现设置正确的关联类型。

此结构中的任何内容都必须实现 TryFromEnv,或者具有一个 #[with()] 注解,该注解指向一个返回实现 TryFromEnvWith 的内容的函数。这些函数还有几个内置于 fho 库或其他 ffx 库中的实现。

此外,工具上方的 #[check()] 注解使用 CheckEnv 的实现来验证命令是否应该在不为结构体本身生成项的情况下运行。此处的 AvailabilityFlag 会检查实验状态,如果未启用,则会提前退出。在编写新的子命令时,应该在其中添加此声明,以防用户在使用它之前就已经使用该子命令。

FfxMain 实现

#[async_trait(?Send)]
impl FfxMain for EchoTool {
    type Writer = MachineWriter<String>;
    async fn main(self, mut writer: Self::Writer) -> Result<()> {
        let text = self.cmd.text.as_deref().unwrap_or("FFX");
        let echo_out = self
            .echo_proxy
            .echo_string(text)
            .await
            .user_message("Error returned from echo service")?;
        writer.item(&echo_out)?;
        Ok(())
    }
}

您可以在此处实现实际的工具逻辑。您可以为 Writer 关联的特征指定一个类型,系统会根据运行 ffx 的上下文(通过 TryFromEnv)为您初始化该类型。大多数新插件都应使用 MachineWriter<> 类型,指定一个不如上述示例 String 的通用类型,但具体做法因工具而异。将来,可能会要求所有新工具都实现机器接口。

此外,此函数的结果类型默认使用 Error 类型,它可用于区分由于用户互动导致的错误和意外错误。这与旧版插件接口区分正常 anyhow 错误和 ffx_errorffx_bail 产生的错误的方式相对应。如需了解详情,请参阅错误文档。

测试

测试子工具的一种常见模式是为 FIDL 协议创建虚假代理。这样,您就可以通过调用代理返回各种结果,而无需实际处理集成测试的复杂性。

    fn setup_fake_echo_proxy() -> ffx::EchoProxy {
        let (proxy, mut stream) =
            fidl::endpoints::create_proxy_and_stream::<ffx::EchoMarker>().unwrap();
        fuchsia_async::Task::local(async move {
            while let Ok(Some(req)) = stream.try_next().await {
                match req {
                    ffx::EchoRequest::EchoString { value, responder } => {
                        responder.send(value.as_ref()).unwrap();
                    }
                }
            }
        })
        .detach();
        proxy
    }

然后,在单元测试中使用此虚构代理

    #[fuchsia::test]
    async fn test_regular_run() {
        const ECHO: &'static str = "foo";
        let cmd = EchoCommand { text: Some(ECHO.to_owned()) };
        let echo_proxy = setup_fake_echo_proxy();
        let test_stdout = TestBuffer::default();
        let writer = MachineWriter::new_buffers(None, test_stdout.clone(), Vec::new());
        let tool = EchoTool { cmd, echo_proxy };
        tool.main(writer).await.unwrap();
        assert_eq!(format!("{ECHO}\n"), test_stdout.into_string());
    }

OWNERS

如果此子工具位于 ffx 树中,则您需要添加一个 OWNERS 文件,用于告知我们谁负责此代码以及如何将其问题分类。代码应如下所示:

file:/path/to/authoritative/OWNERS

最好将它添加为引用(使用 file: 或可能的 include),而不是作为直接的人员列表,这样它就不会因超出限制而变得过时。

添加到 build

如需将该工具作为主机工具添加到 GN build 图中,您需要在 ffx 工具 gn 文件(已添加到 toolstest 组的 public_deps)的主列表中引用该工具。

此后,如果您 fx build ffx,您应该可以在 ffx commands 输出的 Workspace Commands 列表中看到您的工具,并且应该能够运行该工具。

实验性子工具和子命令

建议子工具最初不要在其 BUILD.gn 中包含 sdk_category。这些未指定类别的子工具会被视为“实验性”工具,不属于 SDK build 的一部分。如果用户想要使用二进制文件,则必须直接获得二进制文件。

不过,子命令的处理方式有所不同。

子命令需要在工具中添加 AvailabilityFlag 属性(如需查看示例,请参阅 ffx target update)。如果用户想要使用子命令,则需要设置关联的配置选项才能调用该子命令。

然而,此方法却存在问题,例如缺少对子命令的 FIDL 依赖项的任何验证。因此,自 2023 年 12 月起,处理子命令的机制发生了变化。

与子工具类似,子命令也能够声明其 SDK 类别(默认为“实验性”),以确定子命令是否可用。在构建子工具时,只会包含子工具类别级别或更高级别的子命令。FIDL 依赖项检查将正确验证子命令的要求。

添加到 SDK

在您的工具稳定并且您准备好将其纳入 SDK 中后,您需要将二进制文件添加到 SDK build。请注意,在执行此操作之前,工具必须被视为相对稳定且经过充分测试(在尚未包含在 SDK 中的情况下尽可能多地测试),并且您需要确保已考虑了兼容性问题。

兼容性

在将子工具添加到 SDK 和 IDK 之前,您需要了解以下三个方面:

  1. FIDL 库 - 向 SDK 添加子工具时,必须将您依赖的所有 FIDL 库添加到 SDK。(如需了解详情,请参阅将 API 提升为 partner_internal)。

  2. 命令行参数 - 为了测试由于命令行选项更改而导致的破坏性更改,ArgsInfo 派生宏用于生成命令行的 JSON 表示法。

    这在黄金文件测试中用于检测差异。黄金文件。最终,此测试将得到增强,以便检测破坏向后兼容性的更改并发出警告。

  3. 适合机器的输出 - 工具和子命令需要尽可能具有机器输出,对于测试或构建脚本中使用的工具来说尤其如此。MachineWriter 对象用于协助以 JSON 格式对输出进行编码,并提供用于检测输出结构更改的架构。

    机器输出必须在当前兼容期内保持稳定。最终,系统会对机器输出格式进行黄金检查。使用机器写入器输出的好处是,您可以避免在自由文本中使用不稳定的输出。

更新子工具

如需将子工具添加到 SDK,请将其 BUILD.gn 中的 sdk_category 设置为适当的类别(例如 partner)。如果子工具包含不再处于实验阶段的子命令,请移除其 AvailabilityFlag 属性,这样它们就不再需要调用特殊配置选项。

纳入 SDK

您还需要将您的子工具添加到 SDK GN 文件host_tools 分子中,例如:

sdk_molecule("host_tools") {
  visibility = [ ":*" ]

  _host_tools = [
    ...
    "//path/to/your/tool:sdk", # <-- insert this
    ...
  ]
]

用户体验审核

在将子工具添加到 SDK 和 IDK 之前,Fuchsia 目前还没有正式的“用户体验审核”。设计标准发布后,本文档即会更新。