结构化配置

结构化配置允许 C++/Rust 组件直接在其清单中声明配置架构。使用结构化配置的优势包括:

  • 配置中的错误是在构建和组装时检测到的。
  • 您可以使用同一组件和不同的配置值创建多个软件包。
  • 组件通过静态类型的库读取其配置。
  • 组件框架仅启动配置有效的组件。
  • 可在运行时使用 ffx 工具查看配置。
  • 可在运行时设置值。

如需详细了解结构化配置的实现和行为,请参阅其参考文档

如需在组件中使用结构化配置,您必须更新构建规则、声明架构、定义值并生成客户端库。

更新构建规则

如需在生成客户端库时防止出现循环依赖项,请定义一条编译组件清单的 fuchsia_component_manifest 规则。将此已编译的清单 GN 标签传入 fuchsia_component 规则。

fuchsia_component_manifest("manifest") {
  component_name = "config_example"
  manifest = "meta/config_example.cml"
}

fuchsia_component("component") {
  cm_label = ":manifest"
  deps = [ ":bin" ]
}

声明配置架构

您必须在组件的清单中声明配置架构:

{
    ...
    config: {
        greeting: {
            type: "string",
            max_size: 512,
        },
        delay_ms: { type: "uint64" },
    },

}

结构化配置支持以下类型:

  • bool
  • uint8int8
  • uint16int16
  • uint32int32
  • uint64int64
  • string(需要 max_size 属性)
  • vector(需要 elementmax_count 属性)

如需了解配置架构的完整语法,请参阅 CML 参考文档

组件有了配置架构后,必须使用软件汇编或 GN 为声明的字段定义值。

仅限 Google 员工:如果您的组件针对 eng 和非 eng build 类型进行了不同的配置,则必须先添加结构化配置验证,然后再签入。

定义配置值的方法有两种:在 JSON5 文件中定义,在 GN 中内嵌。

使用软件组装定义配置值

如果组件的配置因产品而异,请参阅组装结构化配置文档。如需了解配置仅因测试和生产环境而异的组件,请参阅下一部分。

使用 GN 定义配置值并打包

fuchsia_structured_config_values GN 模板会根据配置架构验证定义的值,并将它们编译成必须与组件打包在一起的 .cvf 文件。

在 GN 中定义配置值的方法有两种:在 JSON5 文件中定义,或以内嵌方式定义。

JSON5 文件

您可以在 JSON5 文件中写入组件的配置值。由于 JSON5 是 JSON 的严格超集,因此现有的 JSON 配置文件也可以重复用于结构化配置。

JSON 对象中的每个键必须与架构中的一个配置键相对应,且值必须为兼容的 JSON 类型:

{
    // Print "Hello, World!" by default.
    greeting: "World",

    // Sleep for only 100ms by default.
    delay_ms: 100,
}

fuchsia_structured_config_values 规则中提供 JSON5 文件的路径。

fuchsia_structured_config_values("values_from_json_file") {
  cm_label = ":manifest"
  values_source = "../config_example_default_values.json5"
}

内嵌值

fuchsia_structured_config_values 模板还支持以内嵌方式定义配置值:

C++

declare_args() {
  # Set this in args.gn to override the greeting emitted by this example.
  config_example_cpp_greeting = "World"
}

fuchsia_structured_config_values("values_from_gn") {
  cm_label = ":manifest"
  values = {
    greeting = config_example_cpp_greeting
    delay_ms = 100
  }
}

Rust

declare_args() {
  # Set this in args.gn to override the greeting emitted by this example.
  config_example_rust_greeting = "World"
}

fuchsia_structured_config_values("values_from_gn") {
  cm_label = ":manifest"
  values = {
    greeting = config_example_rust_greeting
    delay_ms = 100
  }
}

通过使用 declare_args,您可以在构建时在命令行中更改配置值:

C++

$ fx set core.x64 \
  --with //examples/components/config \
  --args='config_example_cpp_greeting="C++ CLI Override"'

Rust

$ fx set core.x64 \
  --with //examples/components/config \
  --args='config_example_rust_greeting="Rust CLI Override"'

打包组件和值

如需将一个组件和一组值打包在一起,请将 fuchsia_componentfuchsia_structured_config_values 规则添加为 fuchsia_package 的依赖项。

C++

fuchsia_package("cpp_config_example") {
  deps = [
    ":component",
    ":values_from_gn",
  ]
}

Rust

fuchsia_package("rust_config_example") {
  deps = [
    ":component",
    ":values_from_gn",
  ]
}

构建系统会验证组件的配置架构和值文件。配置有误(例如字段不匹配、约束条件有误、缺少值文件)的组件将无法构建。

检查配置

解析组件时,组件管理器会验证组件的配置。

使用 ffx component show 输出组件配置键值对。组件不一定要运行才能实现此功能。

$ ffx component show config_example
                Moniker: /core/ffx-laboratory:config_example
        Component State: Resolved
                      ...
          Configuration: greeting -> "World"
                      ...

CVF 文件必须与组件的配置架构完全匹配。具有部分或缺失配置的组件将无法解析,因此无法启动。

读取配置

组件会使用生成的库读取其已解析的配置值。使用以下构建模板生成库:

C++

executable("bin") {
  # ...
  deps = [
    ":example_config",

    # ...
  ]
}

fuchsia_structured_config_cpp_elf_lib("example_config") {
  cm_label = ":manifest"
}

Rust

rustc_binary("bin") {
  edition = "2021"

  # ...
  deps = [
    ":example_config",

    # ...
  ]
}

fuchsia_structured_config_rust_lib("example_config") {
  cm_label = ":manifest"
}

使用库中的以下函数读取配置值:

C++

// Import the header as if it's located in the same directory as BUILD.gn:
#include "examples/components/config/cpp/example_config.h"

...

// Retrieve configuration
auto c = example_config::Config::TakeFromStartupHandle();

Rust

use example_config::Config;

...

// Retrieve configuration
let config = Config::take_from_startup_handle();

将配置导出到 Inspect

您可以将某个组件的配置导出到 Inspect,以便将其显示在崩溃报告中。客户端库提供了将组件配置导出到检查树的函数:

C++

// Record configuration to inspect
inspect::ComponentInspector inspector(loop.dispatcher(), inspect::PublishOptions{});
inspect::Node config_node = inspector.root().CreateChild("config");
c.RecordInspect(&config_node);

Rust

// Record configuration to inspect
let inspector = fuchsia_inspect::component::inspector();
inspector.root().record_child("config", |config_node| config.record_inspect(config_node));

使用 ffx inspect show 输出组件的导出配置:

$ ffx inspect show core/ffx-laboratory\*config_example
core/ffx-laboratory\:config_example:
  ...
  payload:
    root:
      config:
        greeting = World

来自组件外部的运行时值

默认情况下,组件只会从其解析器返回的 .cvf 文件(通常是来自其软件包)接收配置值。若要将这些值替换为其他组件,它们必须具有 mutability 属性,用于选择启用特定的变更来源:

    config: {
        greeting: {
            // ...
            mutability: [ /* ... */ ],
        },
    },

提供来自父组件的值

当子级选择接收值时,父级组件可以为在集合中启动的子级提供值。

首先,子级必须将 mutability 属性添加到相应的配置字段:

{
    ...
    config: {
        greeting: {
            type: "string",
            max_size: 512,
            mutability: [ "parent" ],
        },
    },

}

使用 Realm 协议启动子项时,将配置值作为替换值传递:

C++

fuchsia_component_decl::Child child_decl;
child_decl.name(child_name);
child_decl.url(kChildUrl);
child_decl.startup(fuchsia_component_decl::StartupMode::kLazy);

fuchsia_component_decl::ConfigOverride greeting_override;
greeting_override.key("greeting");
greeting_override.value(fuchsia_component_decl::ConfigValue::WithSingle(
    fuchsia_component_decl::ConfigSingleValue::WithString(expected_greeting)));
child_decl.config_overrides({{greeting_override}});

fuchsia_component_decl::CollectionRef collection;
collection.name(kCollectionName);

fuchsia_component::CreateChildArgs child_args;

realm->CreateChild({collection, child_decl, std::move(child_args)})
    .ThenExactlyOnce([this](fidl::Result<fuchsia_component::Realm::CreateChild>& result) {
      if (!result.is_ok()) {
        FX_LOGS(ERROR) << "CreateChild failed: " << result.error_value();
        ZX_PANIC("%s", result.error_value().FormatDescription().c_str());
      }
      QuitLoop();
    });
RunLoop();

Rust

let child_decl = Child {
    name: Some(child_name.to_string()),
    url: Some(String::from(CHILD_URL)),
    startup: Some(StartupMode::Lazy),
    config_overrides: Some(vec![ConfigOverride {
        key: Some("greeting".to_string()),
        value: Some(ConfigValue::Single(ConfigSingleValue::String(
            expected_greeting.to_string(),
        ))),
        ..ConfigOverride::default()
    }]),
    ..Child::default()
};

let collection_ref = CollectionRef { name: COLLECTION_NAME.to_string() };
realm
    .create_child(&collection_ref, &child_decl, CreateChildArgs::default())
    .await
    .context("sending create child message")?
    .map_err(|e| anyhow::format_err!("creating child: {e:?}"))?;

如需了解详情,请参阅完整示例

通过 ffx component 提供值

使用 ffx component create 创建组件或使用 ffx component run 运行组件时,您可以替换配置值,就像您在充当父组件一样。

如果运行上述配置示例且未设置任何替换项,系统会将默认配置输出到其日志中:

$ ffx component run /core/ffx-laboratory:hello-default-value fuchsia-pkg://fuchsia.com/rust_config_from_parent_example#meta/config_example.cm --follow-logs
...
[02655.273631][ffx-laboratory:hello-default-value] INFO: Hello, World! (from Rust)

传递 --config 可让您替换该问候语:

$ ffx component run /core/ffx-laboratory:hello-from-parent fuchsia-pkg://fuchsia.com/rust_config_from_parent_example#meta/config_example.cm --config 'greeting="parent component"' --follow-logs
...
[02622.752978][ffx-laboratory:hello-from-parent] INFO: Hello, parent component! (from Rust)`

每个配置字段都指定为 KEY=VALUE,其中 KEY 必须是组件配置架构中的字段,其中包含 mutability: [ "parent" ]VALUE 是可解析为与字段类型匹配的 JSON 字符串。

使用 Realm Builder 进行测试

您可以使用 Realm Builder 动态替换组件的配置值,而不考虑配置字段的 mutability

TODO(https://fxbug.dev/42053123) 添加一个介绍子打包组件用法的部分。

C++

realm_builder.SetConfigValue(child_name, "greeting", "Fuchsia");

Rust

builder.set_config_value(&config_component, "greeting", "Fuchsia".into()).await.unwrap();

Realm Builder 根据组件的配置架构验证替换的值。

动态设置值时,Realm Builder 要求用户选择是否加载已发布组件的打包配置值。

若要在代码中提供值的子集时加载组件的打包值,请执行以下操作:

C++

realm_builder.InitMutableConfigFromPackage(child_name);

Rust

builder.init_mutable_config_from_package(&config_component).await.unwrap();

要在代码中设置组件的所有值而不使用打包的值,请执行以下操作:

C++

realm_builder.InitMutableConfigToEmpty(child_name);

Rust

builder.init_mutable_config_to_empty(&config_component).await.unwrap();