refactor(core): merge firmware and unix cargo packages
What changed, and why it matters
This commit is a routine internal cleanup that merges two separate build packages (one for real hardware firmware and one for the desktop emulator) into a single package. It moves source files into subdirectories and updates build scripts accordingly. There is no user-facing change, no bug fix, and no security-relevant behavior change visible in the diff.
No security action required. Treat as normal build-system refactoring; standard code review and CI validation are sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change refactors the Cargo build structure in trezor-firmware’s core/embed tree. The separate ‘unix’ package used for the emulator is removed, and its sources/configuration are folded into the ‘firmware’ package under a new ‘emulator’ Cargo feature. Source files are relocated under projects/firmware/src/stm32 and projects/firmware/src/unix, include paths are updated, and xtask tooling no longer switches package names based on the emulator flag. Feature validation logic that checked options against the target package’s declared features is also removed. No runtime code semantics appear to change.
Changed components
core/embed/projects/firmware/Cargo.tomlcore/embed/projects/firmware/build.rscore/embed/projects/unix/Cargo.tomlcore/embed/projects/unix/build.rscore/embed/projects/unix/src/main.rscore/embed/projects/unix/version.hcore/embed/upymod/build.rscore/embed/xtask/src/args.rscore/embed/xtask/src/artifacts.rscore/embed/xtask/src/config.rscore/embed/xtask/src/features.rscore/embed/xtask/src/helpers.rsInspect captured patch +48 / −320
### core/embed/Cargo.lock
@@ -1072,22 +1072,6 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-[[package]]
-name = "unix"
-version = "0.0.0"
-dependencies = [
- "color-eyre",
- "crypto",
- "io",
- "models",
- "rtl",
- "sec",
- "sys",
- "trezor_lib",
- "upymod",
- "xbuild",
-]
-
[[package]]
name = "unsize"
version = "1.1.0"
### core/embed/projects/firmware/Cargo.toml
@@ -34,9 +34,11 @@ debug = ["trezor_lib/debug"]
debuglink = ["trezor_lib/debuglink", "io/usb_iface_debug"]
dev_keys = ["trezor_lib/dev_keys"]
disable_animation = ["upymod/disable_animation"]
+emulator = ["io/emulator", "trezor_lib/emulator", "upymod/emulator", "io/kernel_mode", "io/secure_mode", "sys/applets"]
force_bootloader_upgrade = []
frozen = ["upymod/frozen"]
log_stack_usage = ["upymod/log_stack_usage"]
+memperf = ["upymod/memperf"]
micropy_enable_source_lines = ["upymod/enable_source_lines"]
miniscript = ["trezor_lib/miniscript", "upymod/miniscript"]
n1w1 = ["upymod/n1w1"]
### core/embed/projects/firmware/build.rs
@@ -5,18 +5,8 @@ fn main() -> Result<()> {
lib.import_lib("io")?;
lib.import_lib("upymod")?;
- lib.add_includes(["."]);
-
lib.add_include("../../rust"); // Cyclic dependency
- lib.add_sources(["main.c", "header.S", "boot_image_embdata.c"]);
-
- if cfg!(feature = "mcu_stm32") {
- lib.add_source("stm32/coreapp_header.S");
- } else {
- bail_unsupported!()
- }
-
if cfg!(feature = "app_loading") {
lib.add_source("../../api/trezor_api_v1_impl.c");
}
@@ -25,16 +15,29 @@ fn main() -> Result<()> {
lib.add_define("FORCE_BOOTLOADER_UPGRADE", Some("1"));
}
- lib.embed_binary(
- xbuild::vendor_header_path("../../models", "firmware")?,
- "vendorheader",
- )?;
-
- embed_bootloader_binary(lib)?;
- embed_kernel_binary(lib)?;
-
- if cfg!(feature = "nrf") {
- embed_nrf_app_binary(lib)?;
+ if cfg!(feature = "emulator") {
+ lib.add_sources(["src/unix/main.c", "src/unix/main_main.c"]);
+ } else if cfg!(feature = "mcu_stm32") {
+ lib.add_sources([
+ "src/stm32/main.c",
+ "src/stm32/header.S",
+ "src/stm32/boot_image_embdata.c",
+ "src/stm32/coreapp_header.S",
+ ]);
+
+ lib.embed_binary(
+ xbuild::vendor_header_path("../../models", "firmware")?,
+ "vendorheader",
+ )?;
+
+ embed_bootloader_binary(lib)?;
+ embed_kernel_binary(lib)?;
+
+ if cfg!(feature = "nrf") {
+ embed_nrf_app_binary(lib)?;
+ }
+ } else {
+ bail_unsupported!()
}
Ok(())
### core/embed/projects/firmware/src/stm32/boot_image_embdata.c
[binary or diff unavailable]
### core/embed/projects/firmware/src/stm32/coreapp_header.S
[binary or diff unavailable]
### core/embed/projects/firmware/src/stm32/header.S
@@ -1,6 +1,6 @@
.syntax unified
-#include "version.h"
+#include "../../version.h"
.section .header, "a"
### core/embed/projects/firmware/src/stm32/main.c
[binary or diff unavailable]
### core/embed/projects/firmware/src/stm32/mpconfigport.h
[binary or diff unavailable]
### core/embed/projects/firmware/src/stm32/mphalport.c
[binary or diff unavailable]
### core/embed/projects/firmware/src/stm32/mphalport.h
[binary or diff unavailable]
### core/embed/projects/firmware/src/stm32/nlrthumb.c
[binary or diff unavailable]
### core/embed/projects/firmware/src/unix/main.c
@@ -33,7 +33,7 @@
#include <sys/dbg_console.h>
#endif
-#include "version.h"
+#include "../../version.h"
#include <ctype.h>
#include <errno.h>
### core/embed/projects/firmware/src/unix/main_main.c
@@ -80,7 +80,7 @@
#include <SDL3/SDL.h>
-#include "version.h"
+#include "../../version.h"
static void drivers_deinit(void) { flash_deinit(); }
### core/embed/projects/firmware/src/unix/mpconfigport.h
[binary or diff unavailable]
### core/embed/projects/unix/Cargo.toml
@@ -1,171 +0,0 @@
-[package]
-name = "unix"
-version = "0.0.0"
-edition = "2024"
-links = "unix"
-
-[build-dependencies]
-color-eyre.workspace = true
-xbuild.workspace = true
-
-[dependencies]
-crypto.workspace = true
-io.workspace = true
-models.workspace = true
-rtl.workspace = true
-sec.workspace = true
-sys.workspace = true
-trezor_lib.workspace = true
-upymod.workspace = true
-
-[features]
-
-# --------------------------------------------------------------------------
-# Build options
-# --------------------------------------------------------------------------
-
-asan = ["models/asan"]
-benchmark = ["upymod/benchmark", "crypto/aes_gcm"]
-clippy = ["trezor_lib/clippy"]
-dbg_console = ["sys/dbg_console", "trezor_lib/dbg_console"]
-debug = ["trezor_lib/debug"]
-debuglink = ["trezor_lib/debuglink", "io/usb_iface_debug"]
-disable_animation = ["upymod/disable_animation"]
-emulator = ["io/emulator", "trezor_lib/emulator", "upymod/emulator"]
-frozen = ["upymod/frozen"]
-log_stack_usage = ["upymod/log_stack_usage"]
-memperf = ["upymod/memperf"]
-micropy_enable_source_lines = ["upymod/enable_source_lines"]
-miniscript = ["trezor_lib/miniscript", "upymod/miniscript"]
-n1w1 = ["upymod/n1w1"]
-nfc = ["upymod/nfc"]
-optiga_testing = ["sec/optiga_testing"]
-pyopt = ["upymod/pyopt"]
-ui_debug = ["trezor_lib/ui_debug"]
-ui_debug_overlay = ["trezor_lib/ui_debug_overlay"]
-ui_performance_overlay = ["trezor_lib/ui_performance_overlay"]
-universal_fw = ["trezor_lib/universal_fw", "upymod/universal_fw", "io/usb_iface_webauthn"]
-unsafe_fw = []
-
-# --------------------------------------------------------------------------
-# MCU selection - only enable one of these at a time
-# --------------------------------------------------------------------------
-
-mcu_stm32f427 = ["io/mcu_stm32f427", "upymod/mcu_stm32f427", "mcu_stm32f4"]
-mcu_stm32f429 = ["io/mcu_stm32f429", "upymod/mcu_stm32f429", "mcu_stm32f4"]
-mcu_stm32u58 = ["io/mcu_stm32u58", "upymod/mcu_stm32u58", "mcu_stm32u5"]
-mcu_stm32u5g = ["io/mcu_stm32u5g", "upymod/mcu_stm32u5g", "mcu_stm32u5"]
-mcu_stm32u5a = ["io/mcu_stm32u5a", "upymod/mcu_stm32u5a", "mcu_stm32u5"]
-
-# --------------------------------------------------------------------------
-# Layout selection - only enable one of these at a time
-# --------------------------------------------------------------------------
-
-layout_bolt = [
- "trezor_lib/layout_bolt",
- "upymod/layout_bolt",
- "trezor_lib/ui_blurring",
- "trezor_lib/ui_jpeg",
-]
-
-layout_caesar = [
- "trezor_lib/layout_caesar",
- "upymod/layout_caesar",
-]
-
-layout_delizia = [
- "trezor_lib/layout_delizia",
- "upymod/layout_delizia",
- "trezor_lib/ui_blurring",
- "trezor_lib/ui_image_buffer",
- "trezor_lib/ui_overlay",
- "trezor_lib/ui_jpeg",
- "trezor_lib/ui_font_kerning",
-]
-
-layout_eckhart = [
- "trezor_lib/layout_eckhart",
- "upymod/layout_eckhart",
- "trezor_lib/ui_blurring",
- "trezor_lib/ui_image_buffer",
- "trezor_lib/ui_overlay",
- "trezor_lib/ui_jpeg",
- "trezor_lib/ui_font_kerning",
- "trezor_lib/hw_jpeg_decoder",
- "io/hw_jpeg_decoder",
-]
-
-display_mono = ["trezor_lib/display_mono"]
-display_rgb565 = ["trezor_lib/display_rgb565"]
-display_rgba8888 = ["trezor_lib/display_rgba8888", "trezor_lib/ui_color_32bit"]
-
-# --------------------------------------------------------------------------
-# Selectable components
-# --------------------------------------------------------------------------
-
-app_loading = ["io/app_loading", "upymod/app_loading", "trezor_lib/app_loading", "io/ipc"]
-backlight = ["io/backlight", "upymod/backlight", "trezor_lib/backlight"]
-backup_ram = ["sec/backup_ram"]
-ble = ["io/ble", "upymod/ble", "trezor_lib/ble", "nrf", "nrf_auth"]
-boot_ucb = ["sec/boot_ucb", "trezor_lib/boot_ucb"]
-button = ["io/button", "upymod/button", "trezor_lib/button"]
-consumption_mask = ["sec/consumption_mask"]
-dev_keys = ["trezor_lib/dev_keys"]
-display = ["io/display"]
-dma2d = ["io/dma2d", "trezor_lib/dma2d"]
-framebuffer = ["io/framebuffer", "trezor_lib/framebuffer"]
-haptic = ["io/haptic", "upymod/haptic", "trezor_lib/haptic"]
-hash_processor = ["sec/hash_processor"]
-iwdg = ["sec/iwdg"]
-lockable_bootloader = ["io/lockable_bootloader"]
-mcu_attestation = ["sec/mcu_attestation", "upymod/mcu_attestation"]
-nrf = ["io/nrf"]
-nrf_auth = ["io/nrf_auth"]
-optiga = ["sec/optiga", "upymod/optiga", "trezor_lib/optiga"]
-power_manager = ["io/power_manager", "upymod/power_manager", "trezor_lib/power_manager", "trezor_lib/pmic"]
-rgb_led = ["io/rgb_led", "upymod/rgb_led", "trezor_lib/rgb_led"]
-sd_card = ["io/sd_card", "upymod/sd_card", "trezor_lib/sd_card"]
-secmon_layout = ["models/secmon_layout"]
-secret = ["sec/secret"]
-secure_aes = ["sec/secure_aes"]
-serial_number = ["upymod/serial_number", "trezor_lib/serial_number"]
-suspend = ["io/suspend"]
-tamper = ["sec/tamper"]
-telemetry = ["sec/telemetry", "upymod/telemetry", "trezor_lib/telemetry"]
-thp = ["upymod/thp", "crypto/aes_gcm", "trezor_lib/thp"]
-touch = ["io/touch", "upymod/touch", "trezor_lib/touch"]
-touch_wakeup = ["io/touch_wakeup", "upymod/touch_wakeup", "trezor_lib/touch_wakeup"]
-tropic = ["sec/tropic", "upymod/tropic", "trezor_lib/tropic"]
-
-# Legacy coin support
-decred = ["upymod/decred"]
-eos = ["crypto/eos", "upymod/eos"]
-nem = ["crypto/nem", "upymod/nem"]
-
-# --------------------------------------------------------------------------
-# Automatically derived features (do not enable from outside)
-# --------------------------------------------------------------------------
-
-default = [
- "sys/applets",
- "io/display",
- "io/fancy_fatal_error",
- "io/kernel_mode",
- "io/secure_mode",
- "io/usb_iface_wire",
- "sec/rdi",
- "crypto/secp256k1_zkp",
- "crypto/aes_gcm",
-
- "trezor_lib/crypto",
- "trezor_lib/micropython",
- "trezor_lib/protobuf",
- "trezor_lib/ui",
- "trezor_lib/storage",
- "trezor_lib/translations",
- "trezor_lib/usb",
- ]
-
-mcu_stm32f4 = ["mcu_stm32"]
-mcu_stm32u5 = ["sec/trustzone", "mcu_stm32"]
-mcu_stm32 = []
### core/embed/projects/unix/build.rs
@@ -1,20 +0,0 @@
-use xbuild::Result;
-
-fn main() -> Result<()> {
- xbuild::build_and_link("firmware", |lib| {
- lib.import_lib("io")?;
- lib.import_lib("upymod")?;
-
- lib.add_includes(["."]);
-
- lib.add_include("../../rust"); // Cyclic dependency
-
- lib.add_sources(["main.c", "main_main.c"]);
-
- if cfg!(feature = "app_loading") {
- lib.add_source("../../api/trezor_api_v1_impl.c");
- }
-
- Ok(())
- })
-}
### core/embed/projects/unix/src/main.rs
@@ -1,6 +0,0 @@
-#![no_std]
-#![no_main]
-
-// force pull in Rust generated symbols (incl. the panic handler)
-use sys as _;
-use trezor_lib as _;
### core/embed/projects/unix/version.h
@@ -1 +0,0 @@
-../firmware/version.h
\ No newline at end of file
### core/embed/upymod/Cargo.toml
@@ -66,7 +66,7 @@ thp = []
touch = ["io/touch"]
touch_wakeup = ["io/touch_wakeup"]
tropic = ["sec/tropic"]
-universal_fw = ["crypto/universal_fw"]
+universal_fw = ["crypto/universal_fw", "io/usb_iface_webauthn"]
# --------------------------------------------------------------------------
# Automatically derived features (do not enable from outside)
### core/embed/upymod/build.rs
@@ -20,10 +20,10 @@ fn main() -> Result<()> {
// There are two mpconfigport.h files in both ports/unix and projects/unix.
// The first one has precedence and is used for compilation. We need mphalport.h
// from the other.
- lib.add_include("../projects/unix");
+ lib.add_include("../projects/firmware/src/unix");
lib.add_include(PathBuf::from(mpy_dir).join("ports/unix"));
} else if cfg!(feature = "mcu_stm32") {
- lib.add_include("../projects/firmware");
+ lib.add_include("../projects/firmware/src/stm32");
} else {
bail_unsupported!();
}
@@ -272,7 +272,10 @@ fn main() -> Result<()> {
],
);
} else if cfg!(feature = "mcu_stm32") {
- lib.add_sources_in_dir("../projects/firmware", ["mphalport.c", "nlrthumb.c"]);
+ lib.add_sources_in_dir(
+ "../projects/firmware/src/stm32",
+ ["mphalport.c", "nlrthumb.c"],
+ );
lib.add_sources_in_dir(
mpy_dir,
@@ -430,9 +433,9 @@ impl<'a> MpyBuilder<'a> {
// TODO: remove this hack by moving these sources (or part of them)
// into upymod.
let extra_sources = if cfg!(feature = "emulator") {
- [self.crate_dir.join("../projects/unix/main.c")]
+ [self.crate_dir.join("../projects/firmware/src/unix/main.c")]
} else if cfg!(feature = "mcu_stm32") {
- [self.crate_dir.join("../projects/firmware/main.c")]
+ [self.crate_dir.join("../projects/firmware/src/stm32/main.c")]
} else {
bail_unsupported!();
};
### core/embed/xtask/src/args.rs
@@ -23,18 +23,12 @@ pub enum Project {
impl Project {
/// The Rust package name used for cargo commands
- pub fn package_name(self, emulator: bool) -> &'static str {
+ pub fn package_name(self) -> &'static str {
match self {
Project::Bootloader => "bootloader",
Project::Boardloader => "boardloader",
Project::BootloaderCi => "bootloader_ci",
- Project::Firmware => {
- if emulator {
- "unix"
- } else {
- "firmware"
- }
- }
+ Project::Firmware => "firmware",
Project::Prodtest => "prodtest",
Project::Kernel => "kernel",
Project::Secmon => "secmon",
### core/embed/xtask/src/artifacts.rs
@@ -75,7 +75,7 @@ pub fn collect_artifacts(args: &ResolvedBuildArgs, is_dependency: bool) -> Resul
let binary_name = args.project.binary_name();
let profile_dir = helpers::profile_dir(args)?;
let elf = helpers::elf_path(args)?;
- let package = args.project.package_name(args.emulator);
+ let package = args.project.package_name();
let compile_commands = profile_dir.join(format!("{package}.cc.json"));
let elf_ext = if args.emulator { "" } else { ".elf" };
### core/embed/xtask/src/config.rs
@@ -158,7 +158,7 @@ pub struct ProjectConfig {
impl ProjectConfig {
pub fn load(project: Project) -> Result<Self> {
- let pkg = project.package_name(false);
+ let pkg = project.package_name();
let path = workspace_dir()?
.join("projects")
.join(pkg)
@@ -170,26 +170,6 @@ 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)]
@@ -209,7 +189,7 @@ pub fn resolve_board_definition(
emulator: bool,
) -> Result<BoardDefinition> {
let board_config = BoardConfig::load(&model_config.model_id, board_id)?;
- let pkg = project.package_name(false);
+ let pkg = project.package_name();
let model_override = model_config
.project_overrides
.get(pkg)
### core/embed/xtask/src/features.rs
@@ -40,38 +40,19 @@ pub fn resolve_features(args: &ResolvedBuildArgs) -> Result<ResolvedBuildFeature
features.push("emulator".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,
@@ -119,9 +100,9 @@ fn forward_color_choice(cmd: &mut process::Command) {
let color = if env::var_os("NO_COLOR").is_some_and(|v| !v.is_empty()) {
"never"
// https://bixense.com/clicolors
- } else if env::var_os("CLICOLOR_FORCE").is_some_and(|v| !v.is_empty() && v != "0") {
- "always"
- } else if io::stderr().is_terminal() {
+ } else if io::stderr().is_terminal()
+ || env::var_os("CLICOLOR_FORCE").is_some_and(|v| !v.is_empty() && v != "0")
+ {
"always"
} else {
"never"
@@ -135,7 +116,7 @@ pub fn configure_cargo(args: &ResolvedBuildArgs, cmd: &mut process::Command) ->
let resolved = resolve_features(args)?;
let mut rebuild_std = false;
- cmd.args(["--package", args.project.package_name(args.emulator)]);
+ cmd.args(["--package", args.project.package_name()]);
cmd.args(["--features", &resolved.features.join(",")]);
cmd.args(["--profile", args.cargo_profile_name()]);
cmd.env("TREZOR_BOARD_HEADER", &resolved.board_header);
@@ -250,27 +231,6 @@ mod tests {
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
### core/embed/xtask/src/helpers.rs
@@ -9,7 +9,7 @@ use crate::options::ResolvedBuildArgs;
/// Returns the path to the built ELF file for the given build arguments.
pub fn elf_path(args: &ResolvedBuildArgs) -> Result<PathBuf> {
- let elf_name = args.project.package_name(args.emulator);
+ let elf_name = args.project.package_name();
Ok(profile_dir(args)?.join(elf_name))
}
Why this scored 15/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.