開始開發 ffx 子工具

FFX 子工具是 ffx cli 可執行的頂層指令。這些指令可以直接編譯至 ffx 和/或建構為可在建構輸出目錄或 SDK 中找到的個別指令,然後使用 FHO 工具介面叫用。

ffx

放置位置

首先,請在 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 原子。

# 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",
    "//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 = "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

這裡是工具的主要程式碼所在位置。您將在此設定以 argh 為基礎的結構體,用於指令參數,並從結構體中衍生 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,
}

這是工具所需的結構。包括上述定義的引數結構、所需守護程式或裝置的任何 Proxy,或您可能自行定義的其他項目。

這個結構體中必須包含參照上述引數類型的元素,且應具有 #[command] 屬性,以便為 FfxTool 實作設定正確的關聯類型。

這個結構中的任何內容都必須實作 TryFromEnv,或具有 #[with()] 註解,指向可傳回實作 TryFromEnvWith 的函式。fho 程式庫或其他 ffx 程式庫中也內建了這些實作項目。

此外,工具上方的 #[check()] 註解會使用 CheckEnv 的實作項目,驗證指令應在未為結構體本身產生項目的情況下執行。這裡的 AvailabilityFlag 會檢查實驗狀態,並在未啟用實驗功能時提早退出。編寫新的子命令時,應附上這項宣告,以免使用者在子命令可供大眾使用前就依賴它。

FIDL 通訊協定

FFX 子工具可透過 Overnet 使用 FIDL 通訊協定與目標裝置通訊。如要從子工具存取 FIDL 通訊協定,請按照下列步驟操作:

  1. 將 FIDL Rust 繫結新增為子工具的 BUILD.gn 檔案依附元件。以下範例會為 fuchsia.device FIDL 程式庫新增繫結:

      deps = [
        "//sdk/fidl/fuchsia.device:fuchsia.device_rust",
      ]
    
  2. 將必要的繫結匯入子工具實作項目。以下範例會從 fuchsia.device 匯入 NameProviderProxy

    use fidl_fuchsia_device::NameProviderProxy;
    
  3. 宣告工具結構的成員欄位。由於 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 Proxy

測試子工具的常見模式,是為 FIDL 通訊協定建立假的 Proxy。這樣一來,您就能透過呼叫 Proxy 傳回各種結果,而無須實際處理整合測試的複雜性。

    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
    }

然後在單元測試中使用這個假的 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),而非直接列出人員,以免因未使用而過時。

新增至建構

如要將工具新增至 GN 建構圖表做為主機工具,您必須在 ffx 工具 gn 檔案的主要清單中參照該工具,並將其新增至 toolstest 群組的 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 建構。請注意,在執行這項操作前,工具必須相對穩定且經過充分測試 (盡可能不要已將工具納入 SDK),且您必須確實考量相容性問題。

相容性

將子工具新增至 SDK 和 IDK 前,請注意以下三個事項:

  1. FIDL 程式庫:將子工具新增至 SDK 時,您必須將依附於 SDK 的任何 FIDL 程式庫新增至 SDK。(詳情請參閱「將 API 提升至 host_tool」)。

  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
    ...
  ]
]

UX 和 SDK 審查

子工具必須遵循 CLI 指南