FFX 子工具是 ffx cli 可以运行的顶级命令。这些测试可以编译到 ffx 中,也可以构建为单独的命令,这些命令可在 build 输出目录或 SDK 中找到,然后使用 FHO 工具界面调用。
放置位置
首先,在 fuchsia.git 树中的某个位置创建一个目录来存放您的子工具。目前,子工具位于以下位置:
- ffx 插件树,仅包含内置插件和混合插件/子工具。新子工具一般不应放在此处。
- ffx 工具树,仅限外部运行的子工具会放在这里。将其放在此处可让
ffx的维护人员更轻松地协助解决任何问题,或根据ffx与工具之间接口的任何更改来更新您的子工具。 如果您将它放在此处,但 FFX 团队不是此工具的主要维护者,您必须在放置它的目录中放置一个OWNERS文件,其中添加了您团队的组件和一些个人所有者,以便我们知道如何对您工具的问题进行分流。 - 位于项目的自有树中的某个位置。如果 ffx 工具只是现有程序的封装容器,这样做可能是有意义的,但如果您这样做,必须设置
OWNERS文件,以便 FFX 团队可以批准与ffx交互的部分的更新。为此,您可以将file:/src/developer/ffx/OWNERS添加到OWNERS文件中,以覆盖该工具所在的子目录。
除了不将新工具放在插件中之外,具体位置的选择可能需要与工具团队讨论,以确定最佳位置。
哪些文件
确定工具的用途后,创建源文件。最佳实践是将工具的代码分解为一个实现功能的库和一个仅调用该库的 main.rs。
以下文件集将是一个正常的起点:
BUILD.gn
src/lib.rs
src/main.rs
OWNERS
当然,您也可以根据需要将内容拆分到更多库中。请注意,这些示例均基于示例 echo 子工具,但为了简洁起见,部分内容可能已被移除或简化。如果此处有任何内容无法正常运行或看起来不清楚,请查看该目录中的文件。
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 = "2024"
with_unit_tests = true
deps = [
"//src/developer/ffx/fidl:fuchsia.developer.ffx_rust",
"//src/developer/ffx/lib/fho:lib",
"//src/developer/ffx/lib/target/holders:lib",
"//src/developer/ffx/lib/writer: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 = "2024"
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
这是放置工具主要代码的位置。您将在此处设置基于 argh 的命令实参结构,并从将保存工具运行所需上下文的结构中派生出 FfxTool 和 FfxMain 实现。
参数
#[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 会检查实验性状态,如果未启用,则提前退出。编写新的子命令时,应添加此声明,以防止人们在子命令准备好广泛使用之前依赖它。
直接连接
FFX 子工具默认通过 FFX 守护程序进行通信。如果某个子工具需要直接连接到目标,则可以添加声明: ```rust
[derive(FfxTool)]
[target(direct)]
pub struct MyTool { ... } ```
无目标连接
如果某个子工具不需要目标连接,则可以使用 #[target(None)] 进行声明:
```rust
[derive(FfxTool)]
[target(None)]
pub struct MyTool { ... } ```
仅当命令支持 ffx --strict 时,才需要此声明,该声明将验证命令行上是否指定了目标,除非工具具有此声明。不过,对于不与目标设备互动的任何工具,最好都添加此属性。
FIDL 协议
FFX 子工具可以通过 Overnet 使用 FIDL 协议与目标设备通信。如需从子工具访问 FIDL 协议,请执行以下操作:
将 FIDL Rust 绑定作为依赖项添加到子工具的
BUILD.gn文件中。 以下示例为fuchsia.deviceFIDL 库添加了绑定:deps = [ "//sdk/fidl/fuchsia.device:fuchsia.device_rust", ]将必要的绑定导入到子工具实现中。以下示例从
fuchsia.device中导入NameProviderProxy:use fidl_fuchsia_device::NameProviderProxy;声明工具结构的成员字段。由于 FIDL 代理实现了
TryFromEnv特征,因此 FHO 框架将为您创建并初始化该字段。#[derive(FfxTool)] pub struct EchoTool { #[command] cmd: EchoCommand, name_proxy: NameProviderProxy, }
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 更具体的类型,但具体使用哪种类型取决于工具。未来,可能需要所有新工具都实现机器接口。
此外,此函数的结果类型默认使用 fho Error 类型,该类型可用于区分因用户互动而导致的错误和意外错误。如需详细了解相关信息,请参阅错误文档。
单元测试
如果您想对子工具进行单元测试,只需遵循在主机上测试 Rust 代码的标准方法即可。当 with_unit_tests 参数设置为 true 时,ffx_plugin() GN 模板会为单元测试生成 <target_name>_lib_test 库目标。
如果您的 lib.rs 包含测试,可以使用 fx test 调用这些测试:
fx test ffx_example_lib_test如果 fx 测试找不到您的测试,请检查产品配置是否包含您的测试。您可以使用以下命令包含所有 ffx 测试:
fx set ... --with=//src/developer/ffx:tests在测试中使用虚假 FIDL 代理
测试子工具的常见模式是为 FIDL 协议创建虚假代理。这样,您就可以从调用代理返回各种结果,而无需实际处理集成测试的复杂性。
fn setup_fake_echo_proxy() -> ffx::EchoProxy {
let (proxy, mut stream) = fidl::endpoints::create_proxy_and_stream::<ffx::EchoMarker>();
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 文件中的主列表中引用该工具,并将其添加到 tools 和 test 组的 public_deps 中。
之后,如果您运行 fx build ffx,应该能够在 ffx commands 的输出中看到 Workspace Commands 列表中的工具,并且能够运行该工具。
实验性子工具和子命令
建议子工具最初不要在其 BUILD.gn 中包含 sdk_category。未指定类别的这些子工具被视为“内部”工具,不会成为 SDK 的一部分。如果用户想使用该二进制文件,则必须直接向其提供该二进制文件。
不过,子命令的处理方式有所不同。
需要向工具添加 AvailabilityFlag 属性(如需查看示例,请参阅 ffx target update 的历史记录中的提交)。如果用户想要使用子命令,则需要设置关联的配置选项才能调用该子命令。
不过,这种方法存在一些问题,例如无法验证子命令的 FIDL 依赖项。因此,自 2023 年 12 月起,我们正在更改子命令的处理机制。
与子工具类似,子命令将能够声明其 SDK 类别,以确定子命令是否可在 SDK 中使用。 子工具将仅包含类别级别不低于子工具的子命令。FIDL 依赖项检查将正确验证子命令的要求。
添加到 SDK
当您的工具稳定下来并准备好将其纳入 SDK 时,您需要将二进制文件添加到 SDK build 中。请注意,在执行此操作之前,必须认为该工具相对稳定且经过充分测试(尽可能在未将其纳入 SDK 中的情况下进行测试),并且您需要确保已考虑兼容性问题。
兼容性
在将子工具添加到 SDK 和 IDK 之前,您需要注意以下三个方面:
FIDL 库 - 向 SDK 添加子工具时,您必须将所依赖的任何 FIDL 库添加到 SDK。(如需了解详情,请参阅将 API 升级到
host_tool。)命令行实参 - 为了测试因命令行选项更改而导致的中断性变更,ArgsInfo 派生宏用于生成命令行的 JSON 表示形式。
这在黄金文件测试中用于检测差异。黄金文件。最终,此测试将得到增强,以检测并警告会破坏向后兼容性的变更。
机器友好型输出 - 工具和子命令需要尽可能提供机器输出,尤其是在测试或 build 脚本中使用的工具。MachineWriter 对象用于帮助以 JSON 格式对输出进行编码,并提供用于检测输出结构变化的架构。
机器输出必须在当前兼容性窗口内保持稳定。最终,将对机器输出格式进行黄金检查。使用机器撰写器输出的好处在于,您可以自由地在文本中生成不稳定的输出。
更新子工具
如需将子工具添加到 SDK,请将其 BUILD.gn 中的 sdk_category 设置为相应类别(例如 partner)。如果子工具包含不再处于实验阶段的子命令,请移除其 AvailabilityFlag 属性,这样一来,调用这些子命令就不再需要特殊的配置选项。
包含在 SDK 中
您还需要将子工具添加到 SDK GN 文件的 host_tools molecule 中,例如:
sdk_molecule("host_tools") {
visibility = [ ":*" ]
_host_tools = [
...
"//path/to/your/tool:sdk", # <-- insert this
...
]
]
用户体验和 SDK 审核
子工具是遵循 CLI 指南所必需的。