feat(xtask): introduce project-toml build-options
What changed, and why it matters
This commit is a build-system refactoring for Trezor firmware. It moves the mapping of command-line build options to Rust/cargo features out of hard-coded Rust logic and into per-project TOML files. It also adds a small change so debug builds automatically enable the 'debug-link' option. There is no direct vulnerability in the diff, but any mistake in the new TOML mappings could accidentally enable or disable security-relevant features in a shipped firmware image.
Review each project.toml [build-options] mapping for correctness, especially production, debug-link, unsafe-fw, storage-insecure_testing_mode, and btc-only, to ensure no security feature is accidentally enabled or omitted in release builds. Verify the Makefile change does not leak debug-link into non-debug artifacts.
Security signals we found
Build-option mapping now lives in project.toml files, increasing the attack surface for supply-chain/build-configuration tampering
Makefile change automatically enables debug-link for PYOPT=0 debug builds
Validation added to reject storage_insecure_testing_mode in production builds
Validation added to reject options whose mapped features are absent from the target package
No runtime code or cryptographic logic is changed
Evidence from the diff
The change refactors core/embed/xtask: feature_resolver.rs is replaced by features.rs, and each project.toml gains a [build-options] table that maps CLI flags (e.g. production, debug-link, dbg-console, btc-only) to cargo feature lists. A macro-generated options plumbing layer resolves CLI/preset/default values and validates that mapped features exist in the target package. The Makefile now adds –debug-link when PYOPT=0. The old hard-coded feature rules are removed. The commit includes unit tests for validation (e.g. storage_insecure_testing_mode rejected in production, unknown options rejected, non-mappable options rejected).
Changed components
core/Makefilecore/embed/xtask build orchestratorcore/embed/projects/*/project.toml configuration filesInspect captured patch +681 / −408
diff --git a/core/Makefile b/core/Makefile
index 392b5156..49aaae06 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -79,6 +79,7 @@ else ifeq ($(PYOPT),0)
XTASK_BUILD_OPTS += --pyopt false
XTASK_BUILD_OPTS += --disable-animation
XTASK_BUILD_OPTS += --dbg-console vcp
+XTASK_BUILD_OPTS += --debug-link
endif
ifeq ($(TREZOR_MEMPERF),1)
XTASK_BUILD_OPTS += --mem-perf
diff --git a/core/embed/projects/boardloader/project.toml b/core/embed/projects/boardloader/project.toml
index 0dff862d..7bd3085e 100644
--- a/core/embed/projects/boardloader/project.toml
+++ b/core/embed/projects/boardloader/project.toml
@@ -1,3 +1,6 @@
+# Project-specific configuration
+
+# Filter of the model/board defined features for this specific project
uses = [
"backlight",
"boot_ucb",
@@ -16,3 +19,10 @@ uses = [
]
elf_sections = [".vector_table", ".text", ".data", ".rodata", ".capabilities"]
+
+# Build options mapped to the cargo features they activate on this project.
+# vcp is intentionally unmapped: the boardloader has no VCP console.
+[build-options]
+production = { true = ["production"] }
+bootloader-devel = { true = ["bootloader_devel"] }
+dbg-console = { swo = ["dbg_console_swo"], system-view = ["dbg_console_system_view"] }
diff --git a/core/embed/projects/bootloader/project.toml b/core/embed/projects/bootloader/project.toml
index 7893b291..a226535a 100644
--- a/core/embed/projects/bootloader/project.toml
+++ b/core/embed/projects/bootloader/project.toml
@@ -1,3 +1,6 @@
+# Project-specific configuration
+
+# Filter of the model/board defined features for this specific project
uses = [
"backlight",
"backup_ram",
@@ -38,4 +41,15 @@ uses = [
"ui_empty_lock",
]
+# ELF sections to be included in the final binary.
+# The order of sections is important.
elf_sections = [".header", ".flash", ".data"]
+
+# xtask build options mapped to the cargo features
+[build-options]
+bootloader-devel = { true = ["bootloader_devel"] }
+dbg-console = { vcp = ["dbg_console_vcp"], swo = ["dbg_console_swo"], system-view = ["dbg_console_system_view"] }
+debug-link = { true = ["debuglink", "ui_debug"] }
+disable-animation = { true = ["disable_animation"] }
+perf-overlay = { true = ["ui_performance_overlay"] }
+production = { true = ["production"] }
diff --git a/core/embed/projects/bootloader_ci/project.toml b/core/embed/projects/bootloader_ci/project.toml
index 15212f59..b410c0d0 100644
--- a/core/embed/projects/bootloader_ci/project.toml
+++ b/core/embed/projects/bootloader_ci/project.toml
@@ -1,3 +1,6 @@
+# Project-specific configuration
+
+# Filter of the model/board defined features for this specific project
uses = [
"backlight",
"backup_ram",
@@ -22,4 +25,12 @@ uses = [
"touch",
]
+# ELF sections to be included in the final binary.
+# The order of sections is important.
elf_sections = [".header", ".flash", ".data"]
+
+# xtask build options mapped to the cargo features
+[build-options]
+bootloader-devel = { true = ["bootloader_devel"] }
+dbg-console = { vcp = ["dbg_console_vcp"], swo = ["dbg_console_swo"], system-view = ["dbg_console_system_view"] }
+production = { true = ["production"] }
diff --git a/core/embed/projects/firmware/project.toml b/core/embed/projects/firmware/project.toml
index 22e351ea..8b2f4f12 100644
--- a/core/embed/projects/firmware/project.toml
+++ b/core/embed/projects/firmware/project.toml
@@ -1,3 +1,6 @@
+# Project-specific configuration
+
+# Filter of the model/board defined features for this specific project
uses = [
"backlight",
"backup_ram",
@@ -43,8 +46,32 @@ uses = [
"nem",
]
+# ELF sections to be included in the final binary.
+# The order of sections is important.
elf_sections = [".vendorheader", ".header", ".flash", ".data"]
# STM32F4: firmware spans two non-contiguous flash banks (bank2 starts at 0x08100000)
split_pad_to = "0x08100000"
split_part2_sections = [".flash2"]
+
+# xtask build options mapped to the cargo features
+[build-options]
+apps = { true = ["app_loading"] }
+benchmark = { true = ["benchmark"] }
+block-on-vcp = { true = ["block_on_vcp"] }
+bootloader-devel = { true = ["bootloader_devel"] }
+btc-only = { false = ["universal_fw"] }
+dbg-console = { vcp = ["dbg_console"], swo = ["dbg_console"], system-view = ["dbg_console"] }
+debug-link = { true = ["debuglink", "ui_debug"] }
+disable-animation = { true = ["disable_animation"] }
+force-bootloader-upgrade = { true = ["force_bootloader_upgrade"] }
+frozen = { true = ["frozen"] }
+log-stack-usage = { true = ["log_stack_usage"] }
+mem-perf = { true = ["memperf"] }
+n4w1 = { true = ["n4w1"] }
+perf-overlay = { true = ["ui_performance_overlay"] }
+production = { true = ["production"], false = ["dev_keys"] }
+pyopt = { true = ["pyopt"], false = ["debug", "optiga_testing", "ui_debug_overlay"] }
+source-lines = { true = ["micropy_enable_source_lines"] }
+storage-insecure-testing-mode = { true = ["storage_insecure_testing_mode"] }
+unsafe-fw = { true = ["unsafe_fw"] }
diff --git a/core/embed/projects/kernel/project.toml b/core/embed/projects/kernel/project.toml
index 8ca32e85..ec89775f 100644
--- a/core/embed/projects/kernel/project.toml
+++ b/core/embed/projects/kernel/project.toml
@@ -1,3 +1,6 @@
+# Project-specific configuration
+
+# Filter of the model/board defined features for this specific project
uses = [
"backlight",
"backup_ram",
@@ -35,4 +38,20 @@ uses = [
"tropic",
]
+# ELF sections to be included in the final binary.
+# The order of sections is important.
elf_sections = [".flash", ".data"]
+
+# xtask build options mapped to the cargo features
+[build-options]
+apps = { true = ["app_loading"] }
+block-on-vcp = { true = ["block_on_vcp"] }
+bootloader-devel = { true = ["bootloader_devel"] }
+btc-only = { false = ["universal_fw"] }
+dbg-console = { vcp = ["dbg_console_vcp"], swo = ["dbg_console_swo"], system-view = ["dbg_console_system_view"] }
+debug-link = { true = ["debuglink"] }
+force-bootloader-upgrade = { true = ["force_bootloader_upgrade"] }
+production = { true = ["production"] }
+pyopt = { false = ["optiga_testing"] }
+storage-insecure-testing-mode = { true = ["storage_insecure_testing_mode"] }
+unsafe-fw = { true = ["unsafe_fw"] }
diff --git a/core/embed/projects/prodtest/project.toml b/core/embed/projects/prodtest/project.toml
index cd1953cf..caee32f8 100644
--- a/core/embed/projects/prodtest/project.toml
+++ b/core/embed/projects/prodtest/project.toml
@@ -1,3 +1,6 @@
+# Project-specific configuration
+
+# Filter of the model/board defined features for this specific project
uses = [
"backlight",
"backup_ram",
@@ -43,7 +46,17 @@ uses = [
"tropic",
]
+# ELF sections to be included in the final binary.
+# The order of sections is important.
elf_sections = [".vendorheader", ".header", ".flash", ".data"]
secmon_body_sections = [".secmon_header", ".flash", ".data"]
secmon_header_sections = [".vendorheader", ".header"]
+
+# xtask build options mapped to the cargo features.
+[build-options]
+bootloader-devel = { true = ["bootloader_devel"] }
+dbg-console = { swo = ["dbg_console_swo"], system-view = ["dbg_console_system_view"] }
+debug-link = { true = ["debuglink", "ui_debug"] }
+perf-overlay = { true = ["ui_performance_overlay"] }
+production = { true = ["production"] }
diff --git a/core/embed/projects/secmon/project.toml b/core/embed/projects/secmon/project.toml
index f40cc2ff..80c6129c 100644
--- a/core/embed/projects/secmon/project.toml
+++ b/core/embed/projects/secmon/project.toml
@@ -1,3 +1,6 @@
+# Project-specific configuration
+
+# Filter of the model/board defined features for this specific project
uses = [
"backup_ram",
"boot_ucb",
@@ -15,4 +18,17 @@ uses = [
"tropic",
]
+# ELF sections to be included in the final binary.
+# The order of sections is important.
elf_sections = [".secmon_header", ".flash", ".data", ".gnu.sgstubs"]
+
+# xtask build options mapped to the cargo features
+[build-options]
+bootloader-devel = { true = ["bootloader_devel"] }
+btc-only = { false = ["universal_fw"] }
+dbg-console = { swo = ["dbg_console_swo"], system-view = ["dbg_console_system_view"] }
+force-bootloader-upgrade = { true = ["force_bootloader_upgrade"] }
+production = { true = ["production"] }
+pyopt = { false = ["optiga_testing"] }
+storage-insecure-testing-mode = { true = ["storage_insecure_testing_mode"] }
+unsafe-fw = { true = ["unsafe_fw"] }
diff --git a/core/embed/xtask/src/args.rs b/core/embed/xtask/src/args.rs
index c5b6f6e4..965f209e 100644
--- a/core/embed/xtask/src/args.rs
+++ b/core/embed/xtask/src/args.rs
@@ -5,13 +5,14 @@ use serde::Deserialize;
pub use crate::model::Model;
use crate::options::BuildOptions;
-#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
+#[derive(ValueEnum, Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Project {
Bootloader,
Boardloader,
#[value(name = "bootloader_ci")]
BootloaderCi,
+ #[default]
Firmware,
Prodtest,
Kernel,
@@ -104,9 +105,12 @@ impl Project {
}
}
-#[derive(ValueEnum, Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
+#[derive(ValueEnum, Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ConsoleType {
+ /// No debug console
+ #[default]
+ None,
Vcp,
Swo,
SystemView,
diff --git a/core/embed/xtask/src/cargo.rs b/core/embed/xtask/src/cargo.rs
index 869d9b8d..90c132cf 100644
--- a/core/embed/xtask/src/cargo.rs
+++ b/core/embed/xtask/src/cargo.rs
@@ -5,7 +5,7 @@ use owo_colors::OwoColorize;
use crate::args::{BuildArgs, Project, TestArgs};
use crate::options::ResolvedBuildArgs;
-use crate::{artifacts, feature_resolver, helpers, memusage, postbuild, prebuild};
+use crate::{artifacts, features, helpers, memusage, postbuild, prebuild};
pub fn build(args: BuildArgs) -> Result<()> {
let resolved_args = ResolvedBuildArgs::from_build_args(&args)?;
@@ -174,7 +174,7 @@ fn run_cargo_subcommand(subcommand: &str, args: &ResolvedBuildArgs) -> Result<()
cmd.arg(subcommand).current_dir(helpers::workspace_dir()?);
- feature_resolver::configure_cargo(args, &mut cmd)
+ features::configure_cargo(args, &mut cmd)
.context(format!("Failed to construct {} command", subcommand))?;
let project_name = format!("{:?}", args.project).to_lowercase();
diff --git a/core/embed/xtask/src/config.rs b/core/embed/xtask/src/config.rs
index 6449ffa7..9032a050 100644
--- a/core/embed/xtask/src/config.rs
+++ b/core/embed/xtask/src/config.rs
@@ -5,6 +5,7 @@ use serde::Deserialize;
use crate::args::Project;
use crate::helpers::workspace_dir;
+use crate::options::OptionsMap;
#[derive(Deserialize)]
pub struct ModelConfig {
@@ -150,6 +151,9 @@ pub struct ProjectConfig {
pub split_pad_to: Option<String>,
#[serde(default)]
pub split_part2_sections: Option<Vec<String>>,
+ /// The project's complete mapping from build options to cargo features.
+ #[serde(rename = "build-options")]
+ pub options: OptionsMap,
}
impl ProjectConfig {
@@ -166,25 +170,45 @@ impl ProjectConfig {
}
}
+/// Returns the names declared in the `[features]` table of the given
+/// package's Cargo.toml. Used to validate that option-mapped features exist
+/// in the package actually being built.
+pub fn package_features(package: &str) -> Result<HashSet<String>> {
+ let path = workspace_dir()?
+ .join("projects")
+ .join(package)
+ .join("Cargo.toml");
+ let content = std::fs::read_to_string(&path)
+ .with_context(|| format!("Failed to read package manifest: {}", path.display()))?;
+ let manifest: toml::Value = toml::from_str(&content)
+ .with_context(|| format!("Failed to parse package manifest: {}", path.display()))?;
+
+ Ok(manifest
+ .get("features")
+ .and_then(|v| v.as_table())
+ .map(|table| table.keys().cloned().collect())
+ .unwrap_or_default())
+}
+
#[derive(Deserialize, Default, Clone)]
pub struct ModelProjectOverride {
#[serde(default)]
pub exclude: Vec<String>,
}
-pub struct BoardFeatures {
+pub struct BoardDefinition {
pub features: Vec<String>,
pub board_header: String,
}
-pub fn resolve_board_features(
+pub fn resolve_board_definition(
model_config: &ModelConfig,
board_id: &str,
+ project_config: &ProjectConfig,
project: Project,
emulator: bool,
-) -> Result<BoardFeatures> {
+) -> Result<BoardDefinition> {
let board_config = BoardConfig::load(&model_config.model_id, board_id)?;
- let project_config = ProjectConfig::load(project)?;
let pkg = project.package_name(false);
let model_override = model_config
.project_overrides
@@ -238,7 +262,7 @@ pub fn resolve_board_features(
board_config.header
};
- Ok(BoardFeatures {
+ Ok(BoardDefinition {
features,
board_header,
})
diff --git a/core/embed/xtask/src/feature_resolver.rs b/core/embed/xtask/src/feature_resolver.rs
deleted file mode 100644
index a74ed6a2..00000000
--- a/core/embed/xtask/src/feature_resolver.rs
+++ /dev/null
@@ -1,247 +0,0 @@
-use std::process;
-
-use anyhow::{Result, bail};
-
-use crate::args::{ConsoleType, Project};
-use crate::options::ResolvedBuildArgs;
-use crate::{config, helpers};
-
-pub struct ResolvedBuildFeatures {
- pub features: Vec<String>,
- pub target_triple: Option<&'static str>,
- pub board_header: String,
-}
-
-/// Resolves cargo features and target triple from the provided CLI arguments.
-pub fn resolve_features(args: &ResolvedBuildArgs) -> Result<ResolvedBuildFeatures> {
- let mut features: Vec<String> = vec![args.model.feature_name()];
-
- if args.emulator {
- features.push("emulator".into());
-
- if args.asan {
- features.push("asan".into());
- }
- }
-
- if args.production {
- features.push("production".into());
- }
-
- if args.bootloader_devel {
- features.push("bootloader_devel".into());
- }
-
- if args.force_bootloader_upgrade {
- features.push("force_bootloader_upgrade".into());
- }
-
- match (args.project, args.dbg_console) {
- (Project::Firmware, Some(_)) => features.push("dbg_console".into()),
- (Project::Secmon, Some(ConsoleType::Vcp)) => (),
- (Project::Boardloader, Some(ConsoleType::Vcp)) => (),
- (Project::Prodtest, Some(ConsoleType::Vcp)) => (),
- (_, Some(ConsoleType::Vcp)) => features.push("dbg_console_vcp".into()),
- (_, Some(ConsoleType::Swo)) => features.push("dbg_console_swo".into()),
- (_, Some(ConsoleType::SystemView)) => features.push("dbg_console_system_view".into()),
- (_, None) => (),
- }
-
- if args.project == Project::Firmware {
- if args.pyopt {
- features.push("pyopt".into());
- } else {
- features.push("debug".into());
- }
-
- if args.source_lines {
- features.push("micropy_enable_source_lines".into());
- }
-
- if args.benchmark {
- features.push("benchmark".into());
- }
-
- if args.log_stack_usage {
- features.push("log_stack_usage".into());
- }
-
- if args.mem_perf {
- features.push("memperf".into());
- }
-
- if !args.production {
- features.push("dev_keys".into());
- }
-
- if args.n4w1 {
- features.push("n4w1".into());
- }
-
- if args.frozen {
- features.push("frozen".into());
- }
- }
-
- if matches!(args.project, Project::Firmware | Project::Kernel) {
- if args.block_on_vcp {
- features.push("block_on_vcp".into());
- }
-
- if args.apps {
- features.push("app_loading".into());
- }
- }
-
- if matches!(
- args.project,
- Project::Secmon | Project::Kernel | Project::Firmware
- ) {
- if !args.btc_only {
- features.push("universal_fw".into());
- }
-
- if !args.pyopt {
- features.push("optiga_testing".into());
- }
-
- if args.unsafe_fw {
- features.push("unsafe_fw".into());
- }
-
- if args.storage_insecure_testing_mode {
- if args.production {
- bail!("storage_insecure_testing_mode cannot be enabled in production builds");
- }
- features.push("storage_insecure_testing_mode".into());
- }
- }
-
- if matches!(
- args.project,
- Project::Firmware | Project::Bootloader | Project::Prodtest
- ) {
- if args.perf_overlay {
- features.push("ui_performance_overlay".into());
- }
-
- if !args.pyopt {
- features.push("ui_debug_overlay".into());
- }
-
- if args.debug_link {
- features.push("debuglink".into());
- features.push("ui_debug".into());
- }
-
- if args.disable_animation {
- features.push("disable_animation".into());
- }
- }
-
- if matches!(args.project, Project::Kernel) {
- if args.debug_link {
- features.push("debuglink".into());
- }
- }
-
- // Board and model-intrinsic features from TOML config. The emulator emulates
- // the same board it would build for on real hardware (`default_board`, or an
- // explicit `--board`); only the configuration header differs.
- let model_config = args.model.config()?;
- let board_id = args
- .board
- .clone()
- .unwrap_or_else(|| model_config.default_board.clone());
- let board_features =
- config::resolve_board_features(&model_config, &board_id, args.project, args.emulator)?;
- let mut board_feat = board_features.features;
- if args.disable_optiga {
- board_feat.retain(|f| f != "optiga");
- }
- if args.disable_tropic {
- board_feat.retain(|f| f != "tropic");
- }
- features.extend(board_feat);
-
- let target_triple = if args.emulator {
- None
- } else {
- Some(model_config.target_triple()?)
- };
-
- Ok(ResolvedBuildFeatures {
- features,
- target_triple,
- board_header: board_features.board_header,
- })
-}
-
-/// Configures a cargo command with the appropriate arguments and features.
-pub fn configure_cargo(args: &ResolvedBuildArgs, cmd: &mut process::Command) -> Result<()> {
- let resolved = resolve_features(args)?;
- let mut rebuild_std = false;
-
- cmd.args(["--package", args.project.package_name(args.emulator)]);
- cmd.args(["--features", &resolved.features.join(",")]);
- cmd.args(["--profile", args.cargo_profile_name()]);
- cmd.env("TREZOR_BOARD_HEADER", &resolved.board_header);
-
- if args.cargo_profile_name() == "release" {
- // Required by panic-immediate-abort in the release profile
- rebuild_std = true;
- }
-
- if let Some(triple) = resolved.target_triple {
- cmd.args(["--target", triple]);
- }
-
- if args.emit_memory_analysis {
- // See https://nnethercote.github.io/perf-book/type-sizes.html#measuring-type-sizes for more details
- // Also adds an ELF section with Rust functions' stack sizes. See:
- // - https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/emit-stack-sizes.html
- // - https://blog.japaric.io/stack-analysis/
- // - https://github.com/japaric/stack-sizes/
- //
- // Use --config instead of RUSTFLAGS env so that rustflags in .cargo/config.toml
- // are not overridden (RUSTFLAGS env has higher precedence and replaces
- // them entirely).
- cmd.args([
- "--config",
- "build.rustflags=[\"-Zprint-type-sizes\", \"-Zemit-stack-sizes\"]",
- ]);
- }
-
- if args.emulator && args.asan {
- // -Zsanitizer=address is a rustc flag passed via RUSTFLAGS.
- //
- // Without an explicit --target, cargo compiles proc-macros and the firmware in
- // the same pass and RUSTFLAGS leaks into proc-macro crates, causing
- // "can't find crate" errors. Passing --target explicitly (even the same
- // triple as the host) makes cargo separate the host (proc-macros /
- // build scripts) and target (firmware) compilation units, so RUSTFLAGS
- // only reaches the firmware crates.
- cmd.args(["--target", &helpers::host_triple()?]);
- cmd.args([
- "--config",
- "build.rustflags=[\"-Zsanitizer=address\", \"-Clink-arg=-lgcc_s\"]",
- ]);
-
- // Rebuild standard library to be compiled with sanitizer instrumentation
- rebuild_std = true;
- }
-
- if args.timings {
- cmd.arg("--timings");
- }
-
- if args.verbose {
- cmd.arg("--verbose");
- }
-
- if rebuild_std {
- cmd.arg("-Zbuild-std=core");
- }
-
- Ok(())
-}
diff --git a/core/embed/xtask/src/features.rs b/core/embed/xtask/src/features.rs
new file mode 100644
index 00000000..b1b2bff1
--- /dev/null
+++ b/core/embed/xtask/src/features.rs
@@ -0,0 +1,223 @@
+use std::process;
+
+use anyhow::{Result, bail};
+
+use crate::options::ResolvedBuildArgs;
+use crate::{config, helpers};
+
+#[derive(Debug)]
+pub struct ResolvedBuildFeatures {
+ pub features: Vec<String>,
+ pub target_triple: Option<&'static str>,
+ pub board_header: String,
+}
+
+/// Resolves cargo features and target triple from the provided build
+/// arguments.
+///
+/// Option-dependent features come from the `[build-options]` table of the
+/// project's project.toml; board- and model-intrinsic features come from the
+/// model/board TOML configs filtered by the project's `uses` list. Only
+/// features tied to build mechanics (model selection, emulator, asan) are
+/// added directly here.
+pub fn resolve_features(args: &ResolvedBuildArgs) -> Result<ResolvedBuildFeatures> {
+ if args.storage_insecure_testing_mode && args.production {
+ bail!("storage_insecure_testing_mode cannot be enabled in production builds");
+ }
+
+ let mut features: Vec<String> = vec![args.model.feature_name()];
+
+ if args.emulator {
+ features.push("emulator".into());
+
+ if args.asan {
+ features.push("asan".into());
+ }
+ }
+
+ // Option-mapped features, validated against the target package's declared
+ // features so an unsupported option fails here with the option named,
+ // instead of as a cargo error.
+ let project_config = config::ProjectConfig::load(args.project)?;
+ let package = args.project.package_name(args.emulator);
+ let package_features = config::package_features(package)?;
+ for activated in project_config.options.resolve(args) {
+ // Crate-qualified features ("io/foo") belong to dependencies and
+ // can't be checked against this package's feature table.
+ if !activated.feature.contains('/') && !package_features.contains(&activated.feature) {
+ bail!(
+ "option '{}' is not supported by this build: feature '{}' is not defined in package '{}'",
+ activated.option,
+ activated.feature,
+ package
+ );
+ }
+ features.push(activated.feature);
+ }
+
+ // Board and model-intrinsic features from TOML config. The emulator
+ // emulates the same board it would build for on real hardware
+ // (`default_board`, or an explicit `--board`); only the configuration
+ // header differs.
+ let model_config = args.model.config()?;
+
+ let board_id = args
+ .board
+ .clone()
+ .unwrap_or_else(|| model_config.default_board.clone());
+
+ // Get the model/board features filtered by the project's `uses` list.
+ let board_def = config::resolve_board_definition(
+ &model_config,
+ &board_id,
+ &project_config,
+ args.project,
+ args.emulator,
+ )?;
+
+ // Remove features that are disabled by command-line flags.
+ let mut board_features = board_def.features;
+ if args.disable_optiga {
+ board_features.retain(|f| f != "optiga");
+ }
+ if args.disable_tropic {
+ board_features.retain(|f| f != "tropic");
+ }
+ features.extend(board_features);
+
+ let target_triple = if args.emulator {
+ None
+ } else {
+ Some(model_config.target_triple()?)
+ };
+
+ Ok(ResolvedBuildFeatures {
+ features,
+ target_triple,
+ board_header: board_def.board_header,
+ })
+}
+
+/// Configures a cargo command with the appropriate arguments and features.
+pub fn configure_cargo(args: &ResolvedBuildArgs, cmd: &mut process::Command) -> Result<()> {
+ let resolved = resolve_features(args)?;
+ let mut rebuild_std = false;
+
+ cmd.args(["--package", args.project.package_name(args.emulator)]);
+ cmd.args(["--features", &resolved.features.join(",")]);
+ cmd.args(["--profile", args.cargo_profile_name()]);
+ cmd.env("TREZOR_BOARD_HEADER", &resolved.board_header);
+
+ if args.cargo_profile_name() == "release" {
+ // Required by panic-immediate-abort in the release profile
+ rebuild_std = true;
+ }
+
+ if let Some(triple) = resolved.target_triple {
+ cmd.args(["--target", triple]);
+ }
+
+ if args.emit_memory_analysis {
+ // See https://nnethercote.github.io/perf-book/type-sizes.html#measuring-type-sizes for more details
+ // Also adds an ELF section with Rust functions' stack sizes. See:
+ // - https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/emit-stack-sizes.html
+ // - https://blog.japaric.io/stack-analysis/
+ // - https://github.com/japaric/stack-sizes/
+ //
+ // Use --config instead of RUSTFLAGS env so that rustflags in .cargo/config.toml
+ // are not overridden (RUSTFLAGS env has higher precedence and replaces
+ // them entirely).
+ cmd.args([
+ "--config",
+ "build.rustflags=[\"-Zprint-type-sizes\", \"-Zemit-stack-sizes\"]",
+ ]);
+ }
+
+ if args.emulator && args.asan {
+ // -Zsanitizer=address is a rustc flag passed via RUSTFLAGS.
+ //
+ // Without an explicit --target, cargo compiles proc-macros and the firmware in
+ // the same pass and RUSTFLAGS leaks into proc-macro crates, causing
+ // "can't find crate" errors. Passing --target explicitly (even the same
+ // triple as the host) makes cargo separate the host (proc-macros /
+ // build scripts) and target (firmware) compilation units, so RUSTFLAGS
+ // only reaches the firmware crates.
+ cmd.args(["--target", &helpers::host_triple()?]);
+ cmd.args([
+ "--config",
+ "build.rustflags=[\"-Zsanitizer=address\", \"-Clink-arg=-lgcc_s\"]",
+ ]);
+
+ // Rebuild standard library to be compiled with sanitizer instrumentation
+ rebuild_std = true;
+ }
+
+ if args.timings {
+ cmd.arg("--timings");
+ }
+
+ if args.verbose {
+ cmd.arg("--verbose");
+ }
+
+ if rebuild_std {
+ cmd.arg("-Zbuild-std=core");
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::args::Project;
+
+ #[test]
+ fn rejects_insecure_storage_in_production_builds() {
+ let args = ResolvedBuildArgs {
+ production: true,
+ storage_insecure_testing_mode: true,
+ ..ResolvedBuildArgs::default()
+ };
+
+ let error = resolve_features(&args).unwrap_err();
+ assert!(error.to_string().contains("production"));
+ }
+
+ #[test]
+ fn rejects_options_unsupported_by_the_package() {
+ // `memperf` exists only in the unix (emulator) package. The firmware
+ // project maps it, so a hardware build must reject the option up
+ // front instead of failing later inside cargo.
+ let args = ResolvedBuildArgs {
+ frozen: true,
+ pyopt: true,
+ mem_perf: true,
+ ..ResolvedBuildArgs::default()
+ };
+
+ let error = resolve_features(&args).unwrap_err();
+ assert!(
+ error
+ .to_string()
+ .contains("option 'mem-perf' is not supported"),
+ "unexpected error: {error}"
+ );
+ }
+
+ #[test]
+ fn ignores_options_the_project_does_not_map() {
+ // prodtest doesn't map `disable-animation` (the package has no such
+ // feature), so the option is ignored like any other unmapped option.
+ let args = ResolvedBuildArgs {
+ project: Project::Prodtest,
+ frozen: true,
+ pyopt: true,
+ disable_animation: true,
+ ..ResolvedBuildArgs::default()
+ };
+
+ let features = resolve_features(&args).unwrap().features;
+ assert!(!features.contains(&"disable_animation".to_string()));
+ }
+}
diff --git a/core/embed/xtask/src/lib.rs b/core/embed/xtask/src/lib.rs
index 61d94067..d29fdaff 100644
--- a/core/embed/xtask/src/lib.rs
+++ b/core/embed/xtask/src/lib.rs
@@ -3,7 +3,7 @@ pub mod artifacts;
pub mod cargo;
pub mod combine;
pub mod config;
-pub mod feature_resolver;
+pub mod features;
pub mod flash;
pub mod helpers;
pub mod memusage;
diff --git a/core/embed/xtask/src/model.rs b/core/embed/xtask/src/model.rs
index 5bcce6bd..d9d073a8 100644
--- a/core/embed/xtask/src/model.rs
+++ b/core/embed/xtask/src/model.rs
@@ -5,7 +5,7 @@ use serde::Deserialize;
use crate::config::ModelConfig;
use crate::helpers;
-#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
+#[derive(ValueEnum, Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Model {
#[value(name = "d001")]
@@ -25,6 +25,7 @@ pub enum Model {
#[value(name = "t3t2")]
T3T2,
#[value(name = "t3w1")]
+ #[default]
T3W1,
}
diff --git a/core/embed/xtask/src/options.rs b/core/embed/xtask/src/options.rs
index 510ff466..a4c96388 100644
--- a/core/embed/xtask/src/options.rs
+++ b/core/embed/xtask/src/options.rs
@@ -5,248 +5,390 @@ use serde::Deserialize;
use crate::args::{BuildArgs, ConsoleType, Model, Project};
use crate::presets;
-#[derive(Args, Deserialize, Debug, Clone, Default)]
-#[serde(deny_unknown_fields)]
-#[serde(rename_all = "kebab-case")]
-pub struct BuildOptions {
+/// How an option's `Option<T>` value from the defaults/presets/CLI layers is
+/// unwrapped into its [`ResolvedBuildArgs`] field.
+pub trait ResolveValue: Sized {
+ type Resolved;
+ fn resolve(value: Option<Self>) -> Self::Resolved;
+}
+
+/// Flags resolve to plain bools; unset means disabled.
+impl ResolveValue for bool {
+ type Resolved = bool;
+ fn resolve(value: Option<Self>) -> Self::Resolved {
+ value.unwrap_or_default()
+ }
+}
+
+/// Unset resolves to [`ConsoleType::None`] (no debug console).
+impl ResolveValue for ConsoleType {
+ type Resolved = ConsoleType;
+ fn resolve(value: Option<Self>) -> Self::Resolved {
+ value.unwrap_or_default()
+ }
+}
+
+/// The board selection stays optional; unset means the model's default board.
+impl ResolveValue for String {
+ type Resolved = Option<String>;
+ fn resolve(value: Option<Self>) -> Self::Resolved {
+ value
+ }
+}
+
+/// [`OptionsMap`] field type for an option, by kind: `map` options carry
+/// their [`MapValue`] mapping table, `opt` options carry [`NotMappable`],
+/// whose parsing always fails.
+macro_rules! option_map_ty {
+ (map, $ty:ty) => { Option<<$ty as MapValue>::Map> };
+ (opt, $ty:ty) => { NotMappable };
+}
+
+/// [`OptionsMap::resolve`] arm for an option, by kind: `map` options select
+/// features from their mapping table, `opt` options expand to nothing.
+macro_rules! option_resolve_arm {
+ ($activated:ident, $map:expr, $value:expr, map, $name:ident, $ty:ty) => {
+ if let Some(map) = &$map {
+ for feature in <$ty as MapValue>::select(map, $value) {
+ $activated.push(ActivatedFeature {
+ option: stringify!($name).replace('_', "-"),
+ feature: feature.clone(),
+ });
+ }
+ }
+ };
+ ($activated:ident, $map:expr, $value:expr, opt, $name:ident, $ty:ty) => {};
+}
+
+/// Generates the whole option plumbing from a single option list:
+/// [`BuildOptions`] (every option as an overridable `Option<T>` CLI
+/// argument), its `overlay()`, [`ResolvedBuildArgs`] (the project, model and
+/// emulator build parameters plus every option unwrapped per
+/// [`ResolveValue`]), `from_build_args()`, and [`OptionsMap`] with its
+/// `resolve()`.
+///
+/// Each entry is declared as `<kind> <name>: <type>`:
+/// - `map` — the option may be mapped to cargo features in a project.toml
+/// `[build-options]` table (the type must implement [`MapValue`]);
+/// - `opt` — a plain build option; mapping it in project.toml fails to parse.
+macro_rules! build_options {
+ ($($(#[$attr:meta])* $kind:ident $name:ident: $ty:ty),+ $(,)?) => {
+ #[derive(Args, Deserialize, Debug, Clone, Default)]
+ #[serde(deny_unknown_fields)]
+ #[serde(rename_all = "kebab-case")]
+ pub struct BuildOptions {
+ $(
+ $(#[$attr])*
+ pub $name: Option<$ty>,
+ )+
+ }
+
+ impl BuildOptions {
+ /// Overlays `opt` onto `self`; values set in `opt` win.
+ pub fn overlay(self, opt: Self) -> Self {
+ Self {
+ $($name: opt.$name.or(self.$name),)+
+ }
+ }
+ }
+
+ /// Build arguments with the defaults, preset and CLI layers applied.
+ #[derive(Debug, Clone, Default)]
+ pub struct ResolvedBuildArgs {
+ pub project: Project,
+ pub model: Model,
+ pub emulator: bool,
+ $(pub $name: <$ty as ResolveValue>::Resolved,)+
+ }
+
+ impl ResolvedBuildArgs {
+ pub fn from_build_args(args: &BuildArgs) -> Result<Self> {
+ let preset_options = presets::resolve(args)?;
+ let o = preset_options
+ .overlay(args.options.clone());
+
+ Ok(Self {
+ project: args.project,
+ model: args.model,
+ emulator: args.emulator,
+ $($name: <$ty as ResolveValue>::resolve(o.$name),)+
+ })
+ }
+ }
+
+ /// The `[build-options]` table of a project.toml: the project's complete
+ /// mapping from [`BuildOptions`] to cargo features. An option absent
+ /// from the table is ignored by the project. Options declared `opt`
+ /// never map to features; putting them in the table fails at parse
+ /// time.
+ ///
+ /// The schema stays a plain "option value -> feature list" lookup;
+ /// anything needing conditions on other options or build parameters
+ /// belongs in Rust (see `feature_resolver`).
+ #[derive(Deserialize, Debug, Clone, Default)]
+ #[serde(deny_unknown_fields, rename_all = "kebab-case")]
+ pub struct OptionsMap {
+ $(
+ #[serde(default)]
+ pub $name: option_map_ty!($kind, $ty),
+ )+
+ }
+
+ impl OptionsMap {
+ /// Selects the features activated by the resolved option values.
+ /// Options are visited in declaration order for deterministic
+ /// output.
+ pub fn resolve(&self, args: &ResolvedBuildArgs) -> Vec<ActivatedFeature> {
+ let mut activated = Vec::new();
+
+ $(option_resolve_arm!(activated, self.$name, args.$name, $kind, $name, $ty);)+
+
+ activated
+ }
+ }
+ };
+}
+
+build_options! {
/// Enable debug build
#[arg(long, short = 'd', num_args = 0..=1, default_missing_value = "true")]
- pub debug: Option<bool>,
+ opt debug: bool,
/// Debug console backend
#[arg(long)]
- pub dbg_console: Option<ConsoleType>,
+ map dbg_console: ConsoleType,
/// Build Bitcoin-only firmware
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub btc_only: Option<bool>,
+ map btc_only: bool,
/// Enable production build
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub production: Option<bool>,
+ map production: bool,
/// Force bootloader upgrade
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub force_bootloader_upgrade: Option<bool>,
+ map force_bootloader_upgrade: bool,
/// Use dev bootloader
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub bootloader_devel: Option<bool>,
+ map bootloader_devel: bool,
/// Enable unsafe firmware features
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub unsafe_fw: Option<bool>,
+ map unsafe_fw: bool,
/// Embed frozen MicroPython modules
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub frozen: Option<bool>,
+ map frozen: bool,
/// Include MicroPython source lines
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub source_lines: Option<bool>,
+ map source_lines: bool,
/// Optimize MicroPython bytecode
#[arg(long, num_args = 0..=1, default_missing_value = "true", overrides_with = "pyopt")]
- pub pyopt: Option<bool>,
+ map pyopt: bool,
/// Enable Micropython memory performance measurements
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub mem_perf: Option<bool>,
+ map mem_perf: bool,
/// Enable debug link
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub debug_link: Option<bool>,
+ map debug_link: bool,
/// Enable N4W1 support
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub n4w1: Option<bool>,
+ map n4w1: bool,
/// Disable UI animations
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub disable_animation: Option<bool>,
+ map disable_animation: bool,
/// Show UI perf overlay
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub perf_overlay: Option<bool>,
+ map perf_overlay: bool,
/// Include crypto benchmarks
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub benchmark: Option<bool>,
+ map benchmark: bool,
/// Log stack usage
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub log_stack_usage: Option<bool>,
+ map log_stack_usage: bool,
/// Use blocking VCP writes, in order to allow reliable debug data
/// transmission over VCP. Disabled by default, to prevent debug
/// firmware from getting stuck while writing log messages (if the host
/// is not reading them).
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub block_on_vcp: Option<bool>,
+ map block_on_vcp: bool,
/// Enable Address Sanitizer (ASAN) instrumentation
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub asan: Option<bool>,
+ opt asan: bool,
/// Enable external app loading
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub apps: Option<bool>,
+ map apps: bool,
/// Disable OPTIGA support
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub disable_optiga: Option<bool>,
+ opt disable_optiga: bool,
/// Board revision to build for (defaults to model's default_board)
#[arg(long, short = 'b')]
- pub board: Option<String>,
+ opt board: String,
/// Disable TROPIC support
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub disable_tropic: Option<bool>,
+ opt disable_tropic: bool,
/// Enable insecure storage test mode
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub storage_insecure_testing_mode: Option<bool>,
+ map storage_insecure_testing_mode: bool,
/// Emits memory analysis output (type sizes and stack sizes)
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub emit_memory_analysis: Option<bool>,
+ opt emit_memory_analysis: bool,
/// Output cargo timings
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub timings: Option<bool>,
+ opt timings: bool,
/// Enable verbose output
#[arg(long, num_args = 0..=1, default_missing_value = "true")]
- pub verbose: Option<bool>,
-}
-
-impl BuildOptions {
- pub fn overlay(self, opt: Self) -> Self {
- Self {
- debug: opt.debug.or(self.debug),
- dbg_console: opt.dbg_console.or(self.dbg_console),
- btc_only: opt.btc_only.or(self.btc_only),
- production: opt.production.or(self.production),
- force_bootloader_upgrade: opt
- .force_bootloader_upgrade
- .or(self.force_bootloader_upgrade),
- bootloader_devel: opt.bootloader_devel.or(self.bootloader_devel),
- unsafe_fw: opt.unsafe_fw.or(self.unsafe_fw),
- frozen: opt.frozen.or(self.frozen),
- source_lines: opt.source_lines.or(self.source_lines),
- pyopt: opt.pyopt.or(self.pyopt),
- mem_perf: opt.mem_perf.or(self.mem_perf),
- debug_link: opt.debug_link.or(self.debug_link),
- n4w1: opt.n4w1.or(self.n4w1),
- disable_animation: opt.disable_animation.or(self.disable_animation),
- perf_overlay: opt.perf_overlay.or(self.perf_overlay),
- benchmark: opt.benchmark.or(self.benchmark),
- log_stack_usage: opt.log_stack_usage.or(self.log_stack_usage),
- block_on_vcp: opt.block_on_vcp.or(self.block_on_vcp),
- asan: opt.asan.or(self.asan),
- apps: opt.apps.or(self.apps),
- disable_optiga: opt.disable_optiga.or(self.disable_optiga),
- board: opt.board.or(self.board),
- disable_tropic: opt.disable_tropic.or(self.disable_tropic),
- storage_insecure_testing_mode: opt
- .storage_insecure_testing_mode
- .or(self.storage_insecure_testing_mode),
- emit_memory_analysis: opt.emit_memory_analysis.or(self.emit_memory_analysis),
- timings: opt.timings.or(self.timings),
- verbose: opt.verbose.or(self.verbose),
+ opt verbose: bool,
+}
+
+impl ResolvedBuildArgs {
+ /// Determines the Cargo profile to use
+ pub fn cargo_profile_name(&self) -> &'static str {
+ if self.debug {
+ if self.emulator { "dev" } else { "debug-opt" }
+ } else {
+ "release"
}
}
+}
- pub fn postfix(self) -> Self {
- let pyopt = self.pyopt.unwrap_or(true);
- Self {
- debug_link: self.debug_link.or(Some(!pyopt)),
- pyopt: Some(pyopt),
- ..self
+/// Cargo features activated by a boolean build option, per option value.
+/// An omitted key means the value activates no features.
+#[derive(Deserialize, Debug, Clone, Default)]
+#[serde(deny_unknown_fields)]
+pub struct BoolMap {
+ #[serde(rename = "true", default)]
+ pub on: Vec<String>,
+ #[serde(rename = "false", default)]
+ pub off: Vec<String>,
+}
+
+/// Implemented by option value types that can be mapped to cargo features in
+/// a project.toml `[build-options]` table; associates the value type with its
+/// mapping-table representation and selects the features for a value.
+pub trait MapValue: Sized {
+ type Map;
+ fn select(map: &Self::Map, value: Self) -> &[String];
+}
+
+impl MapValue for bool {
+ type Map = BoolMap;
+ fn select(map: &Self::Map, value: Self) -> &[String] {
+ if value { &map.on } else { &map.off }
+ }
+}
+
+impl MapValue for ConsoleType {
+ type Map = ConsoleMap;
+ fn select(map: &Self::Map, value: Self) -> &[String] {
+ match value {
+ ConsoleType::None => &[],
+ ConsoleType::Vcp => &map.vcp,
+ ConsoleType::Swo => &map.swo,
+ ConsoleType::SystemView => &map.system_view,
}
}
}
-#[derive(Debug, Clone)]
-pub struct ResolvedBuildArgs {
- pub project: Project,
- pub model: Model,
- pub emulator: bool,
- pub debug: bool,
- pub dbg_console: Option<ConsoleType>,
- pub btc_only: bool,
- pub production: bool,
- pub force_bootloader_upgrade: bool,
- pub bootloader_devel: bool,
- pub unsafe_fw: bool,
- pub frozen: bool,
- pub source_lines: bool,
- pub pyopt: bool,
- pub mem_perf: bool,
- pub debug_link: bool,
- pub n4w1: bool,
- pub disable_animation: bool,
- pub perf_overlay: bool,
- pub benchmark: bool,
- pub log_stack_usage: bool,
- pub block_on_vcp: bool,
- pub asan: bool,
- pub apps: bool,
- pub disable_optiga: bool,
- pub board: Option<String>,
- pub disable_tropic: bool,
- pub storage_insecure_testing_mode: bool,
- pub emit_memory_analysis: bool,
- pub timings: bool,
- pub verbose: bool,
+/// Cargo features activated by the `dbg-console` option, per console type.
+/// An omitted key means the console type activates no features.
+#[derive(Deserialize, Debug, Clone, Default)]
+#[serde(deny_unknown_fields, rename_all = "kebab-case")]
+pub struct ConsoleMap {
+ #[serde(default)]
+ pub vcp: Vec<String>,
+ #[serde(default)]
+ pub swo: Vec<String>,
+ #[serde(default)]
+ pub system_view: Vec<String>,
}
-impl ResolvedBuildArgs {
- pub fn from_build_args(args: &BuildArgs) -> Result<Self> {
- let preset_options = presets::resolve(args)?;
- let o = preset_options.overlay(args.options.clone()).postfix();
-
- Ok(Self {
- project: args.project,
- model: args.model,
- emulator: args.emulator,
- debug: o.debug.unwrap_or_default(),
- dbg_console: o.dbg_console,
- btc_only: o.btc_only.unwrap_or_default(),
- production: o.production.unwrap_or_default(),
- force_bootloader_upgrade: o.force_bootloader_upgrade.unwrap_or_default(),
- bootloader_devel: o.bootloader_devel.unwrap_or_default(),
- unsafe_fw: o.unsafe_fw.unwrap_or_default(),
- frozen: o.frozen.unwrap_or_default(),
- source_lines: o.source_lines.unwrap_or_default(),
- pyopt: o.pyopt.unwrap_or_default(),
- mem_perf: o.mem_perf.unwrap_or_default(),
- debug_link: o.debug_link.unwrap_or_default(),
- n4w1: o.n4w1.unwrap_or_default(),
- disable_animation: o.disable_animation.unwrap_or_default(),
- perf_overlay: o.perf_overlay.unwrap_or_default(),
- benchmark: o.benchmark.unwrap_or_default(),
- log_stack_usage: o.log_stack_usage.unwrap_or_default(),
- block_on_vcp: o.block_on_vcp.unwrap_or_default(),
- asan: o.asan.unwrap_or_default(),
- apps: o.apps.unwrap_or_default(),
- disable_optiga: o.disable_optiga.unwrap_or_default(),
- board: o.board,
- disable_tropic: o.disable_tropic.unwrap_or_default(),
- storage_insecure_testing_mode: o.storage_insecure_testing_mode.unwrap_or_default(),
- emit_memory_analysis: o.emit_memory_analysis.unwrap_or_default(),
- timings: o.timings.unwrap_or_default(),
- verbose: o.verbose.unwrap_or_default(),
- })
+/// A feature selected from the `[build-options]` table, together with the
+/// option that activated it (for error reporting).
+pub struct ActivatedFeature {
+ pub option: String,
+ pub feature: String,
+}
+
+/// [`OptionsMap`] field type for options declared `opt`: parsing always
+/// fails, so project.toml cannot map these options to cargo features.
+#[derive(Debug, Clone, Default)]
+pub struct NotMappable;
+
+impl<'de> Deserialize<'de> for NotMappable {
+ fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Err(serde::de::Error::custom(
+ "this build option cannot be mapped to cargo features",
+ ))
}
+}
- /// Determines the Cargo profile to use
- pub fn cargo_profile_name(&self) -> &'static str {
- if self.debug {
- if self.emulator { "dev" } else { "debug-opt" }
- } else {
- "release"
- }
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_bool_and_console_maps() {
+ let map: OptionsMap = toml::from_str(
+ r#"
+ production = { true = ["production"], false = ["dev_keys"] }
+ btc-only = { false = ["universal_fw"] }
+ debug-link = { true = ["debuglink", "ui_debug"] }
+ dbg-console = { vcp = ["dbg_console_vcp"], system-view = ["dbg_console_system_view"] }
+ "#,
+ )
+ .unwrap();
+
+ assert_eq!(map.production.as_ref().unwrap().on, ["production"]);
+ assert_eq!(map.production.as_ref().unwrap().off, ["dev_keys"]);
+ assert!(map.btc_only.as_ref().unwrap().on.is_empty());
+ assert_eq!(
+ map.dbg_console.as_ref().unwrap().system_view,
+ ["dbg_console_system_view"]
+ );
+ assert!(map.dbg_console.as_ref().unwrap().swo.is_empty());
+ }
+
+ #[test]
+ fn rejects_unknown_option_names() {
+ let result: Result<OptionsMap, _> = toml::from_str(r#"prodction = { true = ["x"] }"#);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn rejects_non_mappable_options() {
+ // `verbose` is a build option but never maps to features.
+ let result: Result<OptionsMap, _> = toml::from_str(r#"verbose = { true = ["x"] }"#);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn rejects_unknown_console_types() {
+ let result: Result<OptionsMap, _> = toml::from_str(r#"dbg-console = { uart = ["x"] }"#);
+ assert!(result.is_err());
}
}
diff --git a/core/embed/xtask/src/presets.rs b/core/embed/xtask/src/presets.rs
index 56031060..22e55835 100644
--- a/core/embed/xtask/src/presets.rs
+++ b/core/embed/xtask/src/presets.rs
@@ -50,7 +50,7 @@ pub struct PresetsFile {
impl PresetsFile {
fn load(path: &Path) -> Result<Self> {
- let content = fs::read_to_string(&path)
+ let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read build presets: {}", path.display()))?;
toml::from_str(&content)
.with_context(|| format!("Failed to parse build presets: {}", path.display()))
@@ -166,6 +166,21 @@ mod tests {
assert_eq!(options.frozen, Some(true));
}
+ #[test]
+ fn rejects_unknown_option_keys() {
+ // Typos must fail to parse. `deny_unknown_fields` on `Preset` is
+ // inert next to `flatten`; the rejection comes from the flattened
+ // `BuildOptions` denying unknown fields, which serde does not
+ // guarantee — this guards it against serde/toml upgrades.
+ let result: Result<PresetsFile, _> = toml::from_str(
+ r#"
+ [[test]]
+ pyoptt = false
+ "#,
+ );
+ assert!(result.is_err());
+ }
+
#[test]
fn rejects_unknown_preset() {
let presets = PresetsFile::default();
Why this scored 17/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.