From 71e74f75ed4211a38c4132597d75801ec5c32ce9 Mon Sep 17 00:00:00 2001 From: Amit Kumar-Hermosillo Date: Mon, 26 Oct 2026 14:08:14 +0000 Subject: [PATCH] [veer] unify emulator and FPGA tests on caliptra_mcu_hw_model Create macros and rules to define Caliptra images and tests for both emulator and FPGA that transition and cross-compile firmware and tests appropriately for each platform. --- .bazelrc | 9 +- target/veer/BUILD.bazel | 15 +- target/veer/caliptra_emulator_main.rs | 32 -- target/veer/defs.bzl | 41 ++ target/veer/entry.rs | 19 +- target/veer/ipc/user/BUILD.bazel | 27 +- target/veer/syscall_latency/BUILD.bazel | 30 +- target/veer/tests/interrupts/BUILD.bazel | 28 +- target/veer/threads/kernel/BUILD.bazel | 26 +- target/veer/tooling/BUILD.bazel | 109 ++--- target/veer/tooling/caliptra_runner.bzl | 389 ++++++++++++------ target/veer/tooling/caliptra_runner.py | 331 --------------- target/veer/tooling/caliptra_runner_test.py | 25 -- target/veer/tooling/on_device_test.rs | 22 + target/veer/tooling/test_boot_files.rs | 217 ++++++++++ target/veer/unittest_runner/BUILD.bazel | 31 +- .../veer/unittest_runner/host_smoke_test.rs | 32 ++ .../caliptra/caliptra-mcu-sw/BUILD.bazel | 2 +- third_party/caliptra/uprev.py | 4 +- workflows.json | 12 +- 20 files changed, 712 insertions(+), 689 deletions(-) delete mode 100644 target/veer/caliptra_emulator_main.rs delete mode 100755 target/veer/tooling/caliptra_runner.py delete mode 100644 target/veer/tooling/caliptra_runner_test.py create mode 100644 target/veer/tooling/on_device_test.rs create mode 100644 target/veer/tooling/test_boot_files.rs create mode 100644 target/veer/unittest_runner/host_smoke_test.rs diff --git a/.bazelrc b/.bazelrc index 24845bb8e..2dea5ce08 100644 --- a/.bazelrc +++ b/.bazelrc @@ -32,6 +32,7 @@ common --experimental_propagate_custom_flag=@@rules_rust+//rust/settings:extra_e test --@pigweed//pw_kernel:enable_tests=true test --platform_suffix=test +test --test_tag_filters=-fpga,-disabled # Remote execution config definitions # =================================== @@ -117,8 +118,12 @@ test:stress_k_ast1060_evb --test_env=AST1060_EVB_PI_HOST # Platform setup for building //target/veer/* artifacts. common:k_veer --platforms=//target/veer:veer build:k_veer --build_tag_filters=-do_not_build,-kernel_doc_test -# Usage: VCK190_FPGA_HOST= bazel test --config=k_veer //target/veer/unittest_runner:fpga_test -test:k_veer --test_env=VCK190_FPGA_HOST + +# Caliptra VCK190 FPGA board via SSH. +# Host-side test binaries compile for the VCK190's ARM cores (//toolchain/aarch64_linux) +# while MCU firmware images transition to //target/veer:veer with target_type=fpga. +common:fpga --platforms=@pigweed//targets/host:linux_aarch64 +build:fpga --build_tag_filters=-do_not_build,-kernel_doc_test # Site-local overrides. Kept last so developer machine-specific settings (e.g. a # local QEMU source override via --override_repository) take precedence. This diff --git a/target/veer/BUILD.bazel b/target/veer/BUILD.bazel index fbaa54230..bea10d23c 100644 --- a/target/veer/BUILD.bazel +++ b/target/veer/BUILD.bazel @@ -4,7 +4,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "string_flag") load("@pigweed//pw_build:merge_flags.bzl", "flags_from_dict") load("@pigweed//pw_kernel:flags.bzl", "KERNEL_DEVICE_COMMON_FLAGS") -load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_library") load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH") platform( @@ -89,19 +89,6 @@ rust_library( ], ) -rust_binary( - name = "caliptra_emulator", - srcs = ["caliptra_emulator_main.rs"], - edition = "2021", - visibility = [":__subpackages__"], - deps = [ - "//third_party/caliptra/caliptra-mcu-sw:emulator_lib", - "//third_party/caliptra/caliptra-mcu-sw:mcu_testing_common", - "//third_party/caliptra/caliptra-sw:caliptra_emu_cpu", - "@rust_caliptra_crates_host//:clap", - ], -) - rust_library( name = "console", srcs = ["console.rs"], diff --git a/target/veer/caliptra_emulator_main.rs b/target/veer/caliptra_emulator_main.rs deleted file mode 100644 index 0a6acd850..000000000 --- a/target/veer/caliptra_emulator_main.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed under the Apache-2.0 license -// SPDX-License-Identifier: Apache-2.0 - -//! Entry point for the Caliptra MCU emulator. -//! -//! Replaces the upstream `emulator/app/src/main.rs`, which only pumps I3C -//! socket traffic into the emulated target in selected test modes -//! (for example via `--test-feature ...`). Host-side tests drive firmware -//! over the I3C TCP socket (`--i3c-port`), so this entry point unconditionally -//! starts the I3C controller before entering the run loop. -//! -//! Firmware-requested exits terminate the process from within `step()` (the -//! emulator's exit-control peripheral calls `std::process::exit`), so the -//! loop only ends on fatal errors or breakpoints. - -use caliptra_emu_cpu::StepAction; -use caliptra_mcu_emulator::{Emulator, EmulatorArgs}; -use clap::Parser; -use std::io; - -fn main() -> io::Result<()> { - let cli = EmulatorArgs::parse(); - let mut emulator = Emulator::from_args(cli, false)?; - emulator.start_i3c_controller(); - while caliptra_mcu_testing_common::is_emulator_running() { - match emulator.step() { - StepAction::Break | StepAction::Fatal => break, - _ => {} - } - } - Ok(()) -} diff --git a/target/veer/defs.bzl b/target/veer/defs.bzl index 4cb8a016d..10ed03702 100644 --- a/target/veer/defs.bzl +++ b/target/veer/defs.bzl @@ -4,6 +4,10 @@ """Common definitions used by all veer targets. """ +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("//target:unittest_image.bzl", "unittest_image") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_mcu_image") + TARGET_COMPATIBLE_WITH = select({ "//target/veer:target_veer": [], "//conditions:default": ["@platforms//:incompatible"], @@ -30,3 +34,40 @@ def veer_system_config(name, src, **kwargs): tools = [tool], **kwargs ) + +def veer_system_image(name, visibility = None, tags = None, **kwargs): + """Defines a Pigweed `system_image` plus its signed `_image_emulator` and `_image_fpga` `caliptra_mcu_image` targets.""" + system_image( + name = name, + tags = tags, + visibility = visibility, + **kwargs + ) + _veer_mcu_images(name, visibility, tags) + +def veer_unittest_image(name, visibility = None, tags = None, **kwargs): + """Like `veer_system_image`, but wraps the kernel with `unittest_image` so `#[unittest::test]` cases are compiled in.""" + unittest_image( + name = name, + tags = tags, + visibility = visibility, + **kwargs + ) + _veer_mcu_images(name, visibility, tags) + +def _veer_mcu_images(name, visibility, tags): + """Defines the signed `_image_emulator` and `_image_fpga` targets for image `:`.""" + caliptra_mcu_image( + name = name + "_image_emulator", + image = ":" + name, + interface = "emulator", + tags = tags, + visibility = visibility, + ) + caliptra_mcu_image( + name = name + "_image_fpga", + image = ":" + name, + interface = "fpga", + tags = tags, + visibility = visibility, + ) diff --git a/target/veer/entry.rs b/target/veer/entry.rs index cc93fe4c9..fc8652a12 100644 --- a/target/veer/entry.rs +++ b/target/veer/entry.rs @@ -10,8 +10,14 @@ use kernel::{self as _}; #[unsafe(no_mangle)] #[allow(non_snake_case)] pub unsafe extern "C" fn pw_assert_HandleFailure() -> ! { - use kernel::Arch as _; - Arch::panic() + #[cfg(any(feature = "emulator", feature = "fpga"))] + exit(1); + + #[cfg(not(any(feature = "emulator", feature = "fpga")))] + { + use kernel::Arch as _; + Arch::panic() + } } #[riscv_rt::entry] @@ -30,9 +36,12 @@ pub fn exit(code: u32) -> ! { #[cfg(feature = "emulator")] unsafe { - // SAFETY: writing to this address will cause the emulator to exit. - let exitcode = core::ptr::with_exposed_provenance_mut::(0x1000_2000); - exitcode.write_volatile(code); + // SAFETY: 0x1000_1041 is the emulator UART TX register. Writing 0xff + // (pass) or 0x01 (fail) signals ExitStatus to ModelEmulated + // (matching caliptra-mcu-sw/platforms/emulator/rom/src/io.rs::exit_emulator). + let tx = core::ptr::with_exposed_provenance_mut::(0x1000_1041); + let byte: u8 = if code == 0 { 0xff } else { 0x01 }; + tx.write_volatile(byte); } #[cfg(feature = "fpga")] diff --git a/target/veer/ipc/user/BUILD.bazel b/target/veer/ipc/user/BUILD.bazel index 491d895a5..04471213a 100644 --- a/target/veer/ipc/user/BUILD.bazel +++ b/target/veer/ipc/user/BUILD.bazel @@ -1,17 +1,16 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") load("@rules_rust//rust:defs.bzl", "rust_binary") -load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config") -load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_runner", "caliptra_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config", "veer_system_image") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_on_device_test") package(default_visibility = ["//visibility:public"]) -system_image( +veer_system_image( name = "ipc", apps = ["@pigweed//pw_kernel/tests/ipc/user:ipc"], kernel = ":target", @@ -65,22 +64,12 @@ rust_binary( ], ) -caliptra_runner( - name = "ipc_runner_emulator", - interface = "emulator", - tags = ["manual"], - target = ":ipc", -) - -caliptra_test( - name = "ipc_runner_emulator_test", +caliptra_on_device_test( + name = "ipc_test", timeout = "long", - interface = "emulator", - # caliptra_runner.py hardcodes --i3c-port=65534, so caliptra emulator - # tests cannot run in parallel without an EADDRINUSE collision. - tags = [ + interfaces = [ "emulator", - "exclusive", + "fpga", ], - target = ":ipc", + mcu_image = ":ipc", ) diff --git a/target/veer/syscall_latency/BUILD.bazel b/target/veer/syscall_latency/BUILD.bazel index 92110211f..ab727f327 100644 --- a/target/veer/syscall_latency/BUILD.bazel +++ b/target/veer/syscall_latency/BUILD.bazel @@ -2,13 +2,12 @@ # SPDX-License-Identifier: Apache-2.0 load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") -load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") load("@rules_rust//rust:defs.bzl", "rust_binary") -load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config") -load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_runner", "caliptra_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config", "veer_system_image") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_on_device_test") rust_app( name = "syscall_latency", @@ -36,7 +35,7 @@ rust_app( ], ) -system_image( +veer_system_image( name = "measure_syscall_latency", apps = [ ":syscall_latency", @@ -93,26 +92,15 @@ rust_binary_no_panics_test( tags = ["kernel"], ) -caliptra_runner( - name = "measure_syscall_latency_emulator", - interface = "emulator", - tags = ["manual"], - target = ":measure_syscall_latency", -) - -caliptra_test( - name = "measure_syscall_latency_emulator_test", +caliptra_on_device_test( + name = "measure_syscall_latency_test", timeout = "long", - interface = "emulator", # DISABLED: this test hits a pw_kernel terminal exception in kernel mode # (mcause=1, epc=0) on process exit. - # - # caliptra_runner.py hardcodes --i3c-port=65534, so caliptra emulator - # tests cannot run in parallel without an EADDRINUSE collision. - tags = [ - "disabled", + interfaces = [ "emulator", - "exclusive", + "fpga", ], - target = ":measure_syscall_latency", + mcu_image = ":measure_syscall_latency", + tags = ["disabled"], ) diff --git a/target/veer/tests/interrupts/BUILD.bazel b/target/veer/tests/interrupts/BUILD.bazel index 9550938c4..6e9f4b3da 100644 --- a/target/veer/tests/interrupts/BUILD.bazel +++ b/target/veer/tests/interrupts/BUILD.bazel @@ -1,14 +1,13 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") load("@rules_rust//rust:defs.bzl", "rust_binary") -load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config") -load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_runner", "caliptra_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config", "veer_system_image") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_on_device_test") -system_image( +veer_system_image( name = "interrupts", kernel = ":target", platform = "//target/veer", @@ -49,21 +48,12 @@ rust_binary( ], ) -caliptra_runner( - name = "interrupts_runner_emulator", - interface = "emulator", - tags = ["manual"], - target = ":interrupts", -) - -caliptra_test( - name = "emulator_test", - interface = "emulator", - # caliptra_runner.py hardcodes --i3c-port=65534, so caliptra emulator - # tests cannot run in parallel without an EADDRINUSE collision. - tags = [ +# target.rs triggers McuRootBus::PRIMARY_FLASH_CTRL_ERROR_IRQ via the +# emulator-only primary flash controller at 0x2000_8000. +caliptra_on_device_test( + name = "interrupts_test", + interfaces = [ "emulator", - "exclusive", ], - target = ":interrupts", + mcu_image = ":interrupts", ) diff --git a/target/veer/threads/kernel/BUILD.bazel b/target/veer/threads/kernel/BUILD.bazel index 0681718a3..121f1a43c 100644 --- a/target/veer/threads/kernel/BUILD.bazel +++ b/target/veer/threads/kernel/BUILD.bazel @@ -1,15 +1,14 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") load("@rules_rust//rust:defs.bzl", "rust_binary") -load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config") -load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_runner", "caliptra_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config", "veer_system_image") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_on_device_test") -system_image( +veer_system_image( name = "threads", kernel = ":target", platform = "//target/veer", @@ -58,19 +57,12 @@ rust_binary( ], ) -caliptra_runner( - name = "threads_runner_emulator", - interface = "emulator", - tags = ["manual"], - target = ":threads", -) - -caliptra_test( - name = "threads_runner_emulator_test", - interface = "emulator", - tags = [ +caliptra_on_device_test( + name = "threads_test", + interfaces = [ "emulator", - "manual", + "fpga", ], - target = ":threads", + mcu_image = ":threads", + tags = ["manual"], ) diff --git a/target/veer/tooling/BUILD.bazel b/target/veer/tooling/BUILD.bazel index 62aa389d3..d0fe39ef4 100644 --- a/target/veer/tooling/BUILD.bazel +++ b/target/veer/tooling/BUILD.bazel @@ -1,16 +1,20 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -load("@pigweed//pw_build:pw_py_importable_runfile.bzl", "pw_py_importable_runfile") -load("@rules_python//python:defs.bzl", "py_binary", "py_test") -load("@rules_rust//rust:defs.bzl", "rust_binary") -load(":caliptra_runner.bzl", "mcu_rom_wrapper") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") +load(":caliptra_runner.bzl", "caliptra_boot_artifact") + +exports_files( + ["on_device_test.rs"], + visibility = ["//target/veer:__subpackages__"], +) # Fills a system config template with caliptra-mcu-sw memory map values. rust_binary( name = "render_system_config", srcs = ["render_system_config.rs"], edition = "2024", + target_compatible_with = ["@platforms//os:linux"], visibility = ["//target/veer:__subpackages__"], deps = [ "@rust_caliptra_crates_host//:caliptra-mcu-config", @@ -19,71 +23,72 @@ rust_binary( ], ) -mcu_rom_wrapper( - name = "caliptra_rom_transitioned", - target = "//third_party/caliptra/caliptra-sw:caliptra_rom_bin", +# Helper crate that loads the Caliptra/MCU boot binaries listed in the JSON +# `.boot_env` runfile named by CALIPTRA_BOOT_ENV (generated by +# `caliptra_mcu_image`) and constructs `caliptra_mcu_hw_model::InitParams`. +rust_library( + name = "test_boot_files", + srcs = ["test_boot_files.rs"], + edition = "2024", + target_compatible_with = ["@platforms//os:linux"], visibility = ["//target/veer:__subpackages__"], + deps = [ + "//third_party/caliptra/caliptra-mcu-sw:mcu_hw_model", + "@rules_rust//rust/runfiles", + "@rust_caliptra_crates_host//:hex", + "@rust_caliptra_crates_host//:serde_json", + ], ) -pw_py_importable_runfile( - name = "emulator-cptra-rom-runfiles", - src = ":caliptra_rom_transitioned", - executable = False, - import_location = "caliptra.emulator_cptra_rom", +rust_test( + name = "test_boot_files_test", + crate = ":test_boot_files", + edition = "2024", + target_compatible_with = ["@platforms//os:linux"], ) -mcu_rom_wrapper( - name = "caliptra_firmware_bin_transitioned", - target = "//third_party/caliptra/caliptra-sw:caliptra_firmware_bin", +# ── Canonical Emulator Boot Singletons ── + +caliptra_boot_artifact( + name = "caliptra_rom_emulator", + interface = "emulator", + target = "//third_party/caliptra/caliptra-sw:caliptra_rom_bin", visibility = ["//target/veer:__subpackages__"], ) -pw_py_importable_runfile( - name = "emulator-cptra-firmware-runfiles", - src = ":caliptra_firmware_bin_transitioned", - executable = False, - import_location = "caliptra.emulator_cptra_firmware", +caliptra_boot_artifact( + name = "caliptra_firmware_emulator", + interface = "emulator", + target = "//third_party/caliptra/caliptra-sw:caliptra_firmware_bin", + visibility = ["//target/veer:__subpackages__"], ) -mcu_rom_wrapper( - name = "mcu_rom_bin_transitioned", +caliptra_boot_artifact( + name = "mcu_rom_emulator", + interface = "emulator", target = "//third_party/caliptra/caliptra-mcu-sw:mcu_rom_bin", visibility = ["//target/veer:__subpackages__"], ) -pw_py_importable_runfile( - name = "emulator-mcu-rom-runfiles", - src = ":mcu_rom_bin_transitioned", - executable = False, - import_location = "caliptra.emulator_mcu_rom", -) +# ── Canonical FPGA Boot Singletons ── -pw_py_importable_runfile( - name = "emulator-exe-runfiles", - src = "//target/veer:caliptra_emulator", - executable = True, - import_location = "caliptra.emulator_exe", +caliptra_boot_artifact( + name = "caliptra_rom_fpga", + interface = "fpga", + target = "//third_party/caliptra/caliptra-sw:caliptra_rom_bin", + visibility = ["//target/veer:__subpackages__"], ) -py_binary( - name = "caliptra_runner", - srcs = [ - "caliptra_runner.py", - ], - main = "caliptra_runner.py", - deps = [ - "emulator-cptra-firmware-runfiles", - "emulator-cptra-rom-runfiles", - "emulator-exe-runfiles", - "emulator-mcu-rom-runfiles", - "@pigweed//pw_tokenizer/py:detokenize", - "@rules_python//python/runfiles", - ], +caliptra_boot_artifact( + name = "caliptra_firmware_fpga", + interface = "fpga", + target = "//third_party/caliptra/caliptra-sw:caliptra_firmware_bin", + visibility = ["//target/veer:__subpackages__"], ) -py_test( - name = "caliptra_runner_test", - srcs = ["caliptra_runner_test.py"], - imports = ["."], - deps = [":caliptra_runner"], +caliptra_boot_artifact( + name = "mcu_rom_fpga", + interface = "fpga", + target = "//third_party/caliptra/caliptra-mcu-sw:mcu_rom_bin", + visibility = ["//target/veer:__subpackages__"], ) diff --git a/target/veer/tooling/caliptra_runner.bzl b/target/veer/tooling/caliptra_runner.bzl index 87a3b4623..94a8409ed 100644 --- a/target/veer/tooling/caliptra_runner.bzl +++ b/target/veer/tooling/caliptra_runner.bzl @@ -1,54 +1,118 @@ # Licensed under the Apache-2.0 license # SPDX-License-Identifier: Apache-2.0 -"""A rule for running a pigweed system_image in a calptra-mcu development environment""" +"""Rules and macros for building, signing, and testing VeeR MCU images on the Caliptra emulator and VCK190 FPGA.""" load("@pigweed//pw_kernel/tooling:system_image.bzl", "SystemImageInfo") +load("@rules_rust//rust:defs.bzl", "rust_test") -def _target_type_transition_impl(_, attr): - # target_type selects the image's memory map; //third_party/caliptra:platform - # selects the matching Caliptra ROM/FW and MCU ROM variants. - if attr.interface == "fpga": - return { - "//target/veer:target_type": "fpga", - "//third_party/caliptra:platform": "fpga", - } - if attr.interface == "emulator": - return { - "//target/veer:target_type": "emulator", - "//third_party/caliptra:platform": "emulator", - } +DEFAULT_VENDOR_PK_HASH = "b17ca877666657ccd100e6926c7206b60c995cb68992c6c9baefce728af05441dee1ff415adfc187e1e4edb4d3b2d909" + +_INTERFACE_COMPAT = { + "emulator": [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + "fpga": [ + "@platforms//cpu:aarch64", + "@platforms//os:linux", + ], +} + +def _interface_tags(tags, interface, extra = []): + """Returns `tags` plus the `interface` tag and `extra`, deduplicated. + + Interface tags are owned by these macros; passing one explicitly is an + error since it would be applied to every generated interface target. + """ + tags = tags or [] + for t in _INTERFACE_COMPAT: + if t in tags: + fail("Do not pass the '{}' tag; it is added automatically per interface.".format(t)) + return list(set(tags + [interface] + extra)) +def _rlocationpath(ctx, f): + """Returns the rlocation path of `f` (what `$(rlocationpath)` would expand to).""" + if f.short_path.startswith("../"): + return f.short_path[len("../"):] + return ctx.workspace_name + "/" + f.short_path + +def _boot_artifact_transition_impl(_, attr): + # Pin platforms, //target/veer:target_type, and //third_party/caliptra:platform + # to constants so caller flag state never forks a duplicate ST- build + # directory for Caliptra ROM, Caliptra FW (FMC + RT), or MCU ROM. + platform = "fpga" if attr.interface == "fpga" else "emulator" return { - "//target/veer:target_type": "emulator", - "//third_party/caliptra:platform": "emulator", + "//command_line_option:platforms": ["//third_party/caliptra/platforms:caliptra"], + "//target/veer:target_type": platform, + "//third_party/caliptra:platform": platform, } -_target_type_transition = transition( - implementation = _target_type_transition_impl, +_boot_artifact_transition = transition( + implementation = _boot_artifact_transition_impl, inputs = [], outputs = [ + "//command_line_option:platforms", "//target/veer:target_type", "//third_party/caliptra:platform", ], ) -def _mcu_rom_transition_impl(settings, attr): - return {"//command_line_option:platforms": ["//third_party/caliptra/platforms:caliptra"]} +def _caliptra_boot_artifact_impl(ctx): + files = ctx.attr.target[0][DefaultInfo].files.to_list() + return [DefaultInfo(files = depset(files))] + +_caliptra_boot_artifact = rule( + implementation = _caliptra_boot_artifact_impl, + attrs = { + "interface": attr.string( + default = "emulator", + values = ["emulator", "fpga"], + ), + "target": attr.label(cfg = _boot_artifact_transition), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + }, +) + +def caliptra_boot_artifact(name, interface = "emulator", tags = None, target_compatible_with = None, **kwargs): + if target_compatible_with == None: + target_compatible_with = _INTERFACE_COMPAT[interface] + _caliptra_boot_artifact( + name = name, + interface = interface, + tags = _interface_tags(tags, interface), + target_compatible_with = target_compatible_with, + **kwargs + ) + +def _mcu_image_transition_impl(_, attr): + # Pin platforms, //target/veer:target_type, and //third_party/caliptra:platform + # so each MCU system_image is built in a single canonical configuration per + # interface regardless of whether the top-level target is x86_64 or aarch64. + platform = "fpga" if attr.interface == "fpga" else "emulator" + return { + "//command_line_option:platforms": ["//target/veer:veer"], + "//target/veer:target_type": platform, + "//third_party/caliptra:platform": platform, + } -_mcu_rom_transition = transition( - implementation = _mcu_rom_transition_impl, +_mcu_image_transition = transition( + implementation = _mcu_image_transition_impl, inputs = [], - outputs = ["//command_line_option:platforms"], + outputs = [ + "//command_line_option:platforms", + "//target/veer:target_type", + "//third_party/caliptra:platform", + ], ) -def _caliptra_runner_impl(ctx): - system_image_info = ctx.attr.target[SystemImageInfo] +def _caliptra_mcu_image_impl(ctx): + system_image_info = ctx.attr.image[0][SystemImageInfo] bin_file = system_image_info.bin - elf_file = system_image_info.elf - runner = ctx.executable._caliptra_runner - platform = "fpga" if ctx.attr.interface == "fpga" else "emulator" + manifest = ctx.actions.declare_file("{}.manifest".format(ctx.attr.name)) ctx.actions.run( inputs = [ @@ -71,111 +135,186 @@ def _caliptra_runner_impl(ctx): ], ) - run_script = ctx.actions.declare_file(ctx.attr.name + ".sh") - + # JSON map of boot inputs, read by //target/veer/tooling:test_boot_files. + # File paths are rlocation paths for the `runfiles` crate. + boot_env = ctx.actions.declare_file("{}.boot_env".format(ctx.attr.name)) ctx.actions.write( - output = run_script, - is_executable = True, - content = """#!/bin/bash -{runner} --interface {interface} --elf {elf} --bin {bin} --manifest {manifest} --caliptra-rom {caliptra_rom} --caliptra-firmware {caliptra_firmware} --mcu-rom {mcu_rom} --vendor-pk-hash {vendor_pk_hash} {optional_args} -""".format( - runner = runner.short_path, - interface = ctx.attr.interface, - elf = elf_file.short_path, - bin = bin_file.short_path, - manifest = manifest.short_path, - caliptra_rom = ctx.file.caliptra_rom.short_path, - caliptra_firmware = ctx.file.caliptra_firmware.short_path, - mcu_rom = ctx.file.mcu_rom.short_path, - vendor_pk_hash = ctx.attr.vendor_pk_hash, - optional_args = "", - ), + output = boot_env, + content = json.encode_indent({ + "CALIPTRA_FW": _rlocationpath(ctx, ctx.file.caliptra_firmware), + "CALIPTRA_ROM": _rlocationpath(ctx, ctx.file.caliptra_rom), + "MCU_FW": _rlocationpath(ctx, bin_file), + "MCU_ROM": _rlocationpath(ctx, ctx.file.mcu_rom), + "SOC_MANIFEST": _rlocationpath(ctx, manifest), + "VENDOR_PK_HASH": ctx.attr.vendor_pk_hash, + }) + "\n", ) - runner_files_depset = ctx.attr._caliptra_runner[DefaultInfo].default_runfiles.files - - return [DefaultInfo( - runfiles = ctx.runfiles( - files = [ - manifest, - bin_file, - runner, - ctx.file.caliptra_rom, - ctx.file.caliptra_firmware, - ctx.file.mcu_rom, - ], - transitive_files = runner_files_depset, - ), - executable = run_script, - )] - -_BASE_ATTRS = { - "caliptra_firmware": attr.label( - allow_single_file = True, - default = "//target/veer/tooling:caliptra_firmware_bin_transitioned", - doc = "caliptra firmware", - ), - "caliptra_rom": attr.label( - allow_single_file = True, - default = "//target/veer/tooling:caliptra_rom_transitioned", - doc = "caliptra ROM", - ), - "interface": attr.string( - doc = "The interface to use.", - mandatory = True, - ), - "mcu_rom": attr.label( - allow_single_file = True, - default = "//target/veer/tooling:mcu_rom_bin_transitioned", - doc = "MCU ROM", - ), - "target": attr.label( - doc = "The system_image target to run.", - mandatory = True, - providers = [SystemImageInfo], - ), - "vendor_pk_hash": attr.string( - default = "b17ca877666657ccd100e6926c7206b60c995cb68992c6c9baefce728af05441dee1ff415adfc187e1e4edb4d3b2d909", - doc = "SHA384 of vendor public key", - ), - "_caliptra_runner": attr.label( - executable = True, - cfg = "exec", - default = "//target/veer/tooling:caliptra_runner", - ), - "_signer": attr.label( - executable = True, - allow_single_file = True, - cfg = "exec", - default = "//third_party/caliptra/caliptra-mcu-sw:signer", - doc = "caliptra signer", - ), -} - -caliptra_runner = rule( - implementation = _caliptra_runner_impl, - executable = True, - attrs = _BASE_ATTRS, - cfg = _target_type_transition, -) + boot_files = [ + bin_file, + manifest, + ctx.file.caliptra_rom, + ctx.file.caliptra_firmware, + ctx.file.mcu_rom, + ] -caliptra_test = rule( - implementation = _caliptra_runner_impl, - test = True, - attrs = _BASE_ATTRS, - cfg = _target_type_transition, -) - -def _mcu_rom_wrapper_impl(ctx): - files = ctx.attr.target[0][DefaultInfo].files.to_list() - return [DefaultInfo(files = depset(files))] + return [ + DefaultInfo( + files = depset([boot_env]), + runfiles = ctx.runfiles(files = [boot_env] + boot_files), + ), + ] -mcu_rom_wrapper = rule( - implementation = _mcu_rom_wrapper_impl, +_caliptra_mcu_image = rule( + implementation = _caliptra_mcu_image_impl, attrs = { - "target": attr.label(cfg = _mcu_rom_transition), + "caliptra_firmware": attr.label( + allow_single_file = True, + mandatory = True, + ), + "caliptra_rom": attr.label( + allow_single_file = True, + mandatory = True, + ), + "image": attr.label( + cfg = _mcu_image_transition, + mandatory = True, + providers = [SystemImageInfo], + ), + "interface": attr.string( + mandatory = True, + values = ["emulator", "fpga"], + ), + "mcu_rom": attr.label( + allow_single_file = True, + mandatory = True, + ), + "vendor_pk_hash": attr.string( + default = DEFAULT_VENDOR_PK_HASH, + ), "_allowlist_function_transition": attr.label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), + "_signer": attr.label( + allow_single_file = True, + cfg = "exec", + default = "//third_party/caliptra/caliptra-mcu-sw:signer", + executable = True, + ), }, ) + +def caliptra_mcu_image( + name, + image, + interface, + caliptra_rom = "//target/veer/tooling:caliptra_rom_{}", + caliptra_firmware = "//target/veer/tooling:caliptra_firmware_{}", + mcu_rom = "//target/veer/tooling:mcu_rom_{}", + tags = None, + target_compatible_with = None, + **kwargs): + """Transitions a Pigweed system_image to the given interface (emulator or fpga) and signs its SoC manifest once.""" + if interface not in _INTERFACE_COMPAT: + fail("caliptra_mcu_image interface must be 'emulator' or 'fpga', got: {}".format(interface)) + if target_compatible_with == None: + target_compatible_with = _INTERFACE_COMPAT[interface] + + _caliptra_mcu_image( + name = name, + caliptra_firmware = caliptra_firmware.format(interface), + caliptra_rom = caliptra_rom.format(interface), + image = image, + interface = interface, + mcu_rom = mcu_rom.format(interface), + tags = _interface_tags(tags, interface), + target_compatible_with = target_compatible_with, + **kwargs + ) + +def _mcu_image_label(mcu_image, interface): + label = native.package_relative_label(mcu_image) + suffix = "_image_" + interface + if label.name.endswith(suffix) or label.name.endswith("_" + interface): + return label + return label.same_package_label(label.name + suffix) + +def caliptra_test( + name, + srcs, + mcu_image, + interfaces = ["emulator", "fpga"], + deps = [], + data = [], + env = {}, + tags = [], + edition = "2024", + **kwargs): + """Defines host-side rust_test target(s) (_emulator and/or _fpga) that drive `mcu_image` via `caliptra_mcu_hw_model`. + + Args: + name: Base name for the generated `_emulator` and/or `_fpga` test targets. + srcs: Rust test source files (mandatory). + mcu_image: Label of the `veer_system_image` (or `caliptra_mcu_image`) to boot. + interfaces: List of interfaces to generate targets for; subset of `["emulator", "fpga"]`. + deps: Additional Rust dependencies for the test binary. + data: Additional runfile data dependencies. + env: Additional runtime environment variables. + tags: Base tags applied to generated test targets. + edition: Rust edition (defaults to "2024"). + **kwargs: Additional arguments forwarded to `rust_test`. + """ + if not srcs: + fail("caliptra_test requires explicit `srcs` (use caliptra_on_device_test for on-device tests)") + if not interfaces: + fail("caliptra_test requires at least one interface in `interfaces`") + + full_deps = list(deps) + for required_dep in [ + "//target/veer/tooling:test_boot_files", + "//third_party/caliptra/caliptra-mcu-sw:mcu_hw_model", + ]: + if required_dep not in full_deps: + full_deps.append(required_dep) + + for iface in interfaces: + if iface not in _INTERFACE_COMPAT: + fail("Unsupported interface '{}' in caliptra_test (expected 'emulator' or 'fpga')".format(iface)) + + target_name = name if name.endswith("_" + iface) else "{}_{}".format(name, iface) + image_label = _mcu_image_label(mcu_image, iface) + + iface_tags = _interface_tags(tags, iface, ["exclusive"] if iface == "fpga" else []) + + test_env = { + "CALIPTRA_BOOT_ENV": "$(rlocationpath {})".format(image_label), + "RUST_TEST_THREADS": "1", + } + test_env.update(env) + + rust_test( + name = target_name, + srcs = srcs, + data = list(data) + [image_label], + deps = full_deps, + edition = edition, + env = test_env, + tags = iface_tags, + target_compatible_with = _INTERFACE_COMPAT[iface], + **kwargs + ) + +def caliptra_on_device_test( + name, + mcu_image, + interfaces = ["emulator", "fpga"], + **kwargs): + """Defines `_emulator` and/or `_fpga` test targets for an on-device VeeR test image.""" + caliptra_test( + name = name, + srcs = ["//target/veer/tooling:on_device_test.rs"], + crate_root = "//target/veer/tooling:on_device_test.rs", + interfaces = interfaces, + mcu_image = mcu_image, + **kwargs + ) diff --git a/target/veer/tooling/caliptra_runner.py b/target/veer/tooling/caliptra_runner.py deleted file mode 100755 index 06aef5175..000000000 --- a/target/veer/tooling/caliptra_runner.py +++ /dev/null @@ -1,331 +0,0 @@ -# Licensed under the Apache-2.0 license -# SPDX-License-Identifier: Apache-2.0 -"""Utility to invoke the caliptra emulator and pipe the output through the detokenizer.""" - -import argparse -import logging -import os -import pathlib -import subprocess -import sys -import tempfile -import threading -import time -from pathlib import Path - -from pw_tokenizer import detokenize - -_LOG = logging.getLogger(__name__) -_LOG.setLevel(logging.INFO) - -# Environment variable naming the SSH host (user@host or host alias) for the -# VCK190 FPGA board used by the "fpga" interface. -FPGA_HOST = "VCK190_FPGA_HOST" - -# Timeout (seconds) for the remote fpga run. The VeeR core's exit() -# implementation writes the PASS/FAIL sentinel and then spins forever (there's -# no way for it to fully halt itself back to the host), so this bounds how -# long we wait on a stuck or unreachable board rather than hanging forever. -_FPGA_RUN_TIMEOUT_SECONDS = 300 - - -def scan_output_for_result(lines): - """Scan detokenized output lines for a PASS/FAIL sentinel. - - Returns 0 on a line containing "PASS", 1 on a line containing "FAIL", - or None if no sentinel has appeared yet. - """ - for line in lines: - if "PASS" in line: - return 0 - if "FAIL" in line: - return 1 - return None - - -try: - import caliptra.emulator_cptra_firmware # type: ignore - import caliptra.emulator_cptra_rom # type: ignore - import caliptra.emulator_exe # type: ignore - import caliptra.emulator_mcu_rom # type: ignore - from python.runfiles import runfiles # type: ignore - - r = runfiles.Create() - _CPTRA_ROM = r.Rlocation(*caliptra.emulator_cptra_rom.RLOCATION) - _CPTRA_FIRMWARE = r.Rlocation(*caliptra.emulator_cptra_firmware.RLOCATION) - _MCU_ROM = r.Rlocation(*caliptra.emulator_mcu_rom.RLOCATION) - _EMULATOR = r.Rlocation(*caliptra.emulator_exe.RLOCATION) -except ImportError as e: - _LOG.fatal("runfiles could not open resources: %r", e) - - -def _parse_args(): - """Parse and return command line arguments.""" - - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - parser.add_argument( - "--interface", - type=str, - help="interface type", - ) - parser.add_argument( - "--elf", - type=pathlib.Path, - help="elf file ", - ) - parser.add_argument( - "--bin", - type=pathlib.Path, - help="bin file", - ) - parser.add_argument( - "--manifest", - type=pathlib.Path, - help="authorization manifest", - ) - parser.add_argument( - "--caliptra-rom", - type=pathlib.Path, - default=pathlib.Path(_CPTRA_ROM), - help="caliptra ROM binary", - ) - parser.add_argument( - "--caliptra-firmware", - type=pathlib.Path, - default=pathlib.Path(_CPTRA_FIRMWARE), - help="caliptra firmware bundle binary", - ) - parser.add_argument( - "--mcu-rom", - type=pathlib.Path, - default=pathlib.Path(_MCU_ROM), - help="MCU ROM binary", - ) - parser.add_argument( - "--vendor-pk-hash", - type=str, - help="SHA384 of public keys", - ) - - return parser.parse_args() - - -def _detokenizer(image: Path, tokenized_file: Path, finished: threading.Event): - try: - detokenizer = detokenize.Detokenizer(image) - line_buffer = "" - with open(tokenized_file, "r", buffering=1) as f: - while not finished.is_set(): - try: - chunk = f.readline() - if chunk: - # qemu may not write a complete line, so buffer - # the chunks until there is a complete line to - # pass to the detokenizer. - line_buffer += chunk - - # Use a while loop, as there could also potentially - # be multiple lines printed in-between iterations. - while "\n" in line_buffer: - newline_pos = line_buffer.find("\n") + 1 - complete_line = line_buffer[:newline_pos] - if not complete_line.endswith("\r\n"): - complete_line = complete_line.replace("\n", "\r\n") - detokenizer.detokenize_text_to_file( - complete_line, sys.stdout.buffer - ) - sys.stdout.flush() - - line_buffer = line_buffer[newline_pos:] - except BlockingIOError: - # If writing to stdout too fast, it's sometimes possible - # to get BlockingIOError due to the stdout buffer being - # full, so sleep and try again. - time.sleep(0.1) - - # detokenize any remaining data in the buffer. - if line_buffer: - detokenizer.detokenize_text_to_file(complete_line, sys.stdout.buffer) - sys.stdout.flush() - except OSError as e: - print(f"Exception opening file {e}", file=sys.stderr) - - -def load_and_run( - image: Path, - interface: str, - manifest: str, - vendor_pk_hash: str, - elf: Path | None = None, - caliptra_rom: Path | str = _CPTRA_ROM, - caliptra_firmware: Path | str = _CPTRA_FIRMWARE, - mcu_rom: Path | str = _MCU_ROM, -) -> list[str]: - """Prepare arguments to load an image into a board and spawn a console.""" - if interface == "emulator": - cmd = [ - _EMULATOR, - f"--rom={mcu_rom}", - f"--firmware={image}", - f"--caliptra-rom={caliptra_rom}", - f"--caliptra-firmware={caliptra_firmware}", - "--i3c-port=65534", - "--rom-offset=0x80000000", - "--rom-size=0x10000", - "--dccm-offset=0x50000000", - "--dccm-size=0x4000", - "--sram-offset=0x40000000", - "--sram-size=0x100000", - "--pic-offset=0x60000000", - "--i3c-offset=0x20004000", - "--i3c-size=0x1000", - "--mci-offset=0x21000000", - "--mci-size=0xe00000", - "--mbox-offset=0x30020000", - "--mbox-size=0x28", - "--soc-offset=0x30030000", - "--soc-size=0x5e0", - "--otp-offset=0x70000000", - "--otp-size=0x140", - "--lc-offset=0x70000400", - "--lc-size=0x8c", - ] - if manifest and str(manifest) != "None": - cmd.append(f"--soc-manifest={manifest}") - if vendor_pk_hash and str(vendor_pk_hash) != "None": - cmd.append(f"--vendor-pk-hash={vendor_pk_hash}") - return cmd - elif interface == "fpga": - host = os.environ.get(FPGA_HOST) - if not host: - _LOG.fatal("%s is not set; cannot reach the VCK190 board", FPGA_HOST) - sys.exit(1) - - remote_bin = f"/tmp/{Path(image).name}" - try: - subprocess.run(["scp", str(image), f"{host}:{remote_bin}"], check=True) - except subprocess.CalledProcessError as e: - _LOG.fatal("Failed to copy %s to %s: %s", image, host, e) - sys.exit(1) - - # Loads the image into the MCU ROM backdoor SRAM, deasserts - # cptra_ss_rst_b, and streams the debug FIFO back over stdout. - # See hw/fpga/README.md's "JTAG debug" section and - # hw/fpga/kernel-modules/mcu_rom_backdoor.c for the mechanism. - cmd = [ - "ssh", - host, - "sudo", - "caliptra-mcu-sw/hw/fpga/launch_openocd.sh", - "load-and-run", - remote_bin, - ] - _LOG.info("Invoking fpga runner: %s", cmd) - try: - proc = subprocess.run( - cmd, - capture_output=True, - text=True, - check=False, - timeout=_FPGA_RUN_TIMEOUT_SECONDS, - ) - except subprocess.TimeoutExpired as e: - _LOG.fatal( - "fpga runner timed out after %s seconds; stdout so far: %s; " - "stderr so far: %s", - _FPGA_RUN_TIMEOUT_SECONDS, - e.stdout, - e.stderr, - ) - sys.exit(1) - - if proc.stderr: - _LOG.info("fpga runner stderr: %s", proc.stderr) - - if proc.returncode != 0: - _LOG.fatal( - "ssh/remote command failed with exit code %d: %s", - proc.returncode, - proc.stderr, - ) - sys.exit(1) - - # This target's kernel config pins its log backend to - # log_backend_basic (a plain-text logger; see target/veer/BUILD.bazel's - # platform rule), so the board's console output is never tokenized and - # there is nothing to detokenize here, unlike the emulator's tokenized - # console path (see _detokenizer() above). - print(proc.stdout) - result = scan_output_for_result(proc.stdout.splitlines()) - if result is None: - _LOG.fatal( - "Device produced no PASS/FAIL sentinel; fpga runner stderr: %s", - proc.stderr, - ) - sys.exit(1) - sys.exit(result) - else: - raise Exception("unknown mechanism", mechanism) - - -def simple_console(cmd: list[str]): - """Invoke for a simple (non-tokenized) console.""" - _LOG.info("Invoking mcu emulator: %s", cmd) - return subprocess.run(cmd, check=False).returncode - - -def tokenized_console(cmd: list[str]): - """Invoke for a tokenized console.""" - _LOG.info("Invoking mcu emulator: %s", cmd) - with tempfile.NamedTemporaryFile() as f: - with subprocess.Popen( - args=cmd, - stdout=f, - ) as proc: - # Capturing the sub process stdout or stderr and then writing to - # stdout can cause deadlocks (see - # https://docs.python.org/3/library/subprocess.html#subprocess.Popen.stderr) - # due to a write buffer (child process) filling up the pipe - # buffer before the parent process can consume it. - # To work around this, write to a temp file, and have the - # detokenizer poll and detokenize the temp file. - finished_event = threading.Event() - stdout_thread = threading.Thread( - target=_detokenizer, - args=(Path(args.elf), Path(f.name), finished_event), - daemon=True, - ) - stdout_thread.start() - out, err = proc.communicate() - finished_event.set() - if out: - print(out) - if err: - print(err) - return_code = proc.returncode - stdout_thread.join() - return return_code - - -def _main(args) -> int: - cmd = load_and_run( - args.bin, - args.interface, - args.manifest, - args.vendor_pk_hash, - elf=args.elf, - caliptra_rom=args.caliptra_rom, - caliptra_firmware=args.caliptra_firmware, - mcu_rom=args.mcu_rom, - ) - # TODO(cfrantz): add support for the tokenized console. - return_code = simple_console(cmd) - sys.exit(return_code) - - -if __name__ == "__main__": - logging.basicConfig() - _main(_parse_args()) diff --git a/target/veer/tooling/caliptra_runner_test.py b/target/veer/tooling/caliptra_runner_test.py deleted file mode 100644 index 212e05765..000000000 --- a/target/veer/tooling/caliptra_runner_test.py +++ /dev/null @@ -1,25 +0,0 @@ -# Licensed under the Apache-2.0 license -# SPDX-License-Identifier: Apache-2.0 -import unittest - -from caliptra_runner import scan_output_for_result - - -class ScanOutputForResultTest(unittest.TestCase): - def test_no_sentinel_yet(self): - self.assertIsNone(scan_output_for_result(["booting...", "init i3c"])) - - def test_pass_sentinel(self): - self.assertEqual(0, scan_output_for_result(["running tests", "PASS"])) - - def test_fail_sentinel(self): - self.assertEqual(1, scan_output_for_result(["running tests", "FAIL: 1"])) - - def test_first_sentinel_wins(self): - self.assertEqual( - 0, scan_output_for_result(["PASS", "unrelated trailing noise"]) - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/target/veer/tooling/on_device_test.rs b/target/veer/tooling/on_device_test.rs new file mode 100644 index 000000000..d86eaa1ce --- /dev/null +++ b/target/veer/tooling/on_device_test.rs @@ -0,0 +1,22 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Shared host-side test entry point for on-device VeeR tests +//! (`caliptra_on_device_test`). +//! +//! Boots the configured MCU firmware image through `caliptra_mcu_hw_model` +//! (`ModelEmulated` on x86_64, `ModelFpgaRealtime` on VCK190 aarch64) and +//! steps it until the image exits with PASS or FAIL. MCU UART output is logged +//! by the hardware model's `log_writer` (stdout). + +use caliptra_mcu_hw_model::McuHwModel; +use test_boot_files::BootFiles; + +#[test] +fn on_device_test() { + let files = BootFiles::from_env(); + let mut hw = + caliptra_mcu_hw_model::new(files.init_params()).expect("hardware model failed to boot"); + hw.step_until_exit_success() + .expect("on-device test firmware did not exit with PASS"); +} diff --git a/target/veer/tooling/test_boot_files.rs b/target/veer/tooling/test_boot_files.rs new file mode 100644 index 000000000..d4ed42e31 --- /dev/null +++ b/target/veer/tooling/test_boot_files.rs @@ -0,0 +1,217 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Loads Caliptra and MCU boot binaries listed in the JSON `.boot_env` runfile +//! generated by `caliptra_mcu_image` (located via `CALIPTRA_BOOT_ENV`) and +//! constructs [`caliptra_mcu_hw_model::InitParams`]. + +use std::collections::HashMap; +use std::path::PathBuf; + +use caliptra_mcu_hw_model::{Fuses, InitParams}; +use runfiles::{rlocation, Runfiles}; + +/// Boot binaries and vendor public key hash for one `caliptra_mcu_image`. +pub struct BootFiles { + pub caliptra_rom: Vec, + pub caliptra_fw: Vec, + pub mcu_rom: Vec, + pub mcu_fw: Vec, + pub soc_manifest: Vec, + pub vendor_pk_hash: [u8; 48], +} + +impl BootFiles { + /// Loads all boot inputs from the `.boot_env` runfile named by + /// `CALIPTRA_BOOT_ENV`. + /// + /// Panics if the runfile or any file it references cannot be read, or if + /// its contents are malformed. + pub fn from_env() -> Self { + Self::try_from_env().expect("failed to load boot files") + } + + /// Fallible variant of [`Self::from_env`]. + pub fn try_from_env() -> Result { + let runfiles = Runfiles::create().map_err(|e| format!("locating runfiles: {e:?}"))?; + let boot_env = std::env::var("CALIPTRA_BOOT_ENV") + .map_err(|_| "CALIPTRA_BOOT_ENV is not set".to_string())?; + Self::load(&boot_env, |path| rlocation!(runfiles, path)) + } + + /// Loads boot inputs from the `.boot_env` at rlocation path `boot_env`, + /// using `resolve` to map rlocation paths to filesystem paths. + fn load(boot_env: &str, resolve: impl Fn(&str) -> Option) -> Result { + let read = |rlocation_path: &str| -> Result, String> { + let path = resolve(rlocation_path) + .ok_or_else(|| format!("runfile not found: {rlocation_path}"))?; + std::fs::read(&path).map_err(|e| format!("reading {}: {e}", path.display())) + }; + + let contents = String::from_utf8(read(boot_env)?) + .map_err(|e| format!("{boot_env} is not UTF-8: {e}"))?; + let entries: HashMap = + serde_json::from_str(&contents).map_err(|e| format!("parsing {boot_env}: {e}"))?; + let get = |key: &str| { + entries + .get(key) + .map(String::as_str) + .ok_or_else(|| format!("{boot_env} is missing {key}")) + }; + + let hash_bytes = hex::decode(get("VENDOR_PK_HASH")?) + .map_err(|e| format!("invalid hex in VENDOR_PK_HASH: {e}"))?; + let vendor_pk_hash: [u8; 48] = hash_bytes.try_into().map_err(|v: Vec| { + format!("VENDOR_PK_HASH must be 48 bytes, got {} bytes", v.len()) + })?; + + Ok(Self { + caliptra_rom: read(get("CALIPTRA_ROM")?)?, + caliptra_fw: read(get("CALIPTRA_FW")?)?, + mcu_rom: read(get("MCU_ROM")?)?, + mcu_fw: read(get("MCU_FW")?)?, + soc_manifest: read(get("SOC_MANIFEST")?)?, + vendor_pk_hash, + }) + } + + /// Returns default [`InitParams`] configured to boot this image through the + /// Caliptra + MCU ROM recovery flow (`active_mode: true`, + /// `enable_mcu_uart_log: true`, `check_booted_to_runtime: true`). + pub fn init_params(&self) -> InitParams<'_> { + // Fuses take the vendor PK hash as big-endian words (matching upstream + // caliptra-mcu-sw's test harness). + let mut fuse_hash = [0u32; 12]; + for (word, chunk) in fuse_hash + .iter_mut() + .zip(self.vendor_pk_hash.chunks_exact(4)) + { + if let &[a, b, c, d] = chunk { + *word = u32::from_be_bytes([a, b, c, d]); + } + } + InitParams { + fuses: Fuses { + vendor_pk_hash: fuse_hash, + ..Default::default() + }, + caliptra_rom: &self.caliptra_rom, + caliptra_firmware: &self.caliptra_fw, + mcu_rom: &self.mcu_rom, + mcu_firmware: &self.mcu_fw, + soc_manifest: &self.soc_manifest, + vendor_pk_hash: Some(self.vendor_pk_hash), + active_mode: true, + enable_mcu_uart_log: true, + check_booted_to_runtime: true, + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + const SAMPLE_HASH_HEX: &str = "000102030405060708090a0b0c0d0e0f\ + 101112131415161718191a1b1c1d1e1f\ + 202122232425262728292a2b2c2d2e2f"; + + fn temp_dir(label: &str) -> PathBuf { + let base = std::env::var_os("TEST_TMPDIR") + .map(PathBuf::from) + .unwrap_or_else(std::env::temp_dir); + let dir = base.join(format!("test_boot_files_{}_{label}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + dir + } + + /// Writes boot binaries and a `.boot_env` under `root`, returning the + /// `.boot_env` rlocation path. `boot_env_json` overrides its contents. + fn write_fixture(root: &Path, boot_env_json: Option<&str>) -> &'static str { + let pkg = root.join("_main/pkg"); + std::fs::create_dir_all(&pkg).expect("create fixture tree"); + for (name, contents) in [ + ("cptra_rom.bin", "cptra_rom"), + ("cptra_fw.bin", "cptra_fw"), + ("mcu_rom.bin", "mcu_rom"), + ("mcu_fw.bin", "mcu_fw"), + ("soc.manifest", "manifest"), + ] { + std::fs::write(pkg.join(name), contents).unwrap(); + } + let default_json = format!( + r#"{{ + "CALIPTRA_ROM": "_main/pkg/cptra_rom.bin", + "CALIPTRA_FW": "_main/pkg/cptra_fw.bin", + "MCU_ROM": "_main/pkg/mcu_rom.bin", + "MCU_FW": "_main/pkg/mcu_fw.bin", + "SOC_MANIFEST": "_main/pkg/soc.manifest", + "VENDOR_PK_HASH": "{SAMPLE_HASH_HEX}" + }}"# + ); + std::fs::write( + pkg.join("image.boot_env"), + boot_env_json.unwrap_or(&default_json), + ) + .unwrap(); + "_main/pkg/image.boot_env" + } + + fn load_from(root: &Path, boot_env: &str) -> Result { + BootFiles::load(boot_env, |p| Some(root.join(p))) + } + + #[test] + fn loads_boot_files_and_packs_big_endian_fuses() { + let root = temp_dir("ok"); + let boot_env = write_fixture(&root, None); + let files = load_from(&root, boot_env).expect("load boot files"); + + assert_eq!(files.caliptra_rom, b"cptra_rom"); + assert_eq!(files.caliptra_fw, b"cptra_fw"); + assert_eq!(files.mcu_rom, b"mcu_rom"); + assert_eq!(files.mcu_fw, b"mcu_fw"); + assert_eq!(files.soc_manifest, b"manifest"); + + let params = files.init_params(); + assert_eq!(params.fuses.vendor_pk_hash[0], 0x0001_0203); + assert_eq!(params.fuses.vendor_pk_hash[1], 0x0405_0607); + assert_eq!(params.fuses.vendor_pk_hash[11], 0x2c2d_2e2f); + assert_eq!(params.vendor_pk_hash, Some(files.vendor_pk_hash)); + assert!(params.active_mode); + assert!(params.enable_mcu_uart_log); + assert!(params.check_booted_to_runtime); + } + + #[test] + fn rejects_malformed_boot_env() { + let root = temp_dir("errors"); + let cases = [ + ("not json", "parsing"), + (r#"{"CALIPTRA_ROM": "_main/pkg/cptra_rom.bin"}"#, "missing"), + ( + r#"{"VENDOR_PK_HASH": "not-valid-hex"}"#, + "invalid hex in VENDOR_PK_HASH", + ), + ( + r#"{"VENDOR_PK_HASH": "00112233"}"#, + "VENDOR_PK_HASH must be 48 bytes, got 4 bytes", + ), + ]; + for (json, expected) in cases { + let boot_env = write_fixture(&root, Some(json)); + let err = load_from(&root, boot_env).err().expect("expected error"); + assert!(err.contains(expected), "{json:?}: unexpected error {err}"); + } + } + + #[test] + fn reports_missing_runfile() { + let err = BootFiles::load("_main/pkg/image.boot_env", |_| None) + .err() + .expect("expected error"); + assert!(err.contains("runfile not found"), "unexpected error {err}"); + } +} diff --git a/target/veer/unittest_runner/BUILD.bazel b/target/veer/unittest_runner/BUILD.bazel index 343b08e10..85e7e011c 100644 --- a/target/veer/unittest_runner/BUILD.bazel +++ b/target/veer/unittest_runner/BUILD.bazel @@ -4,11 +4,10 @@ load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") load("@rules_rust//rust:defs.bzl", "rust_binary") -load("//target:unittest_image.bzl", "unittest_image") -load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config") -load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH", "veer_system_config", "veer_unittest_image") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_on_device_test", "caliptra_test") -unittest_image( +veer_unittest_image( name = "unittest_runner", kernel = ":target", platform = "//target/veer", @@ -21,27 +20,23 @@ target_linker_script( template = "//target/veer:linker_script_template", ) -caliptra_test( - name = "emulator_test", - interface = "emulator", - # caliptra_runner.py hardcodes --i3c-port=65534, so caliptra emulator - # tests cannot run in parallel without an EADDRINUSE collision. - tags = [ +caliptra_on_device_test( + name = "unittest_runner_test", + interfaces = [ "emulator", - "exclusive", + "fpga", ], - target = ":unittest_runner", + mcu_image = ":unittest_runner", ) caliptra_test( - name = "fpga_test", - interface = "fpga", - tags = [ + name = "host_smoke_test", + srcs = ["host_smoke_test.rs"], + interfaces = [ + "emulator", "fpga", - "manual", - "requires-fpga", ], - target = ":unittest_runner", + mcu_image = ":unittest_runner", ) veer_system_config( diff --git a/target/veer/unittest_runner/host_smoke_test.rs b/target/veer/unittest_runner/host_smoke_test.rs new file mode 100644 index 000000000..27762eb99 --- /dev/null +++ b/target/veer/unittest_runner/host_smoke_test.rs @@ -0,0 +1,32 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Host-driven test exercising `caliptra_mcu_hw_model` (`ModelEmulated` +//! on x86_64, `ModelFpgaRealtime` on VCK190 aarch64) against the shared +//! `//target/veer/unittest_runner` MCU firmware image. + +use caliptra_mcu_hw_model::{McuHwModel, McuManager}; +use test_boot_files::BootFiles; + +#[test] +fn probe_mci_and_boot() { + let files = BootFiles::from_env(); + let mut hw = caliptra_mcu_hw_model::new_unbooted(files.init_params()) + .expect("hardware model failed to initialize"); + let (hw_rev_id, wires0, wires1, security_state) = hw.mcu_manager().with_mci(|mci| { + ( + u32::from(mci.hw_rev_id().read()), + mci.generic_input_wires().at(0).read(), + mci.generic_input_wires().at(1).read(), + u32::from(mci.security_state().read()), + ) + }); + eprintln!( + "MCI hw_rev_id={hw_rev_id:#010x} generic_input_wires=[{wires0:#010x}, {wires1:#010x}] security_state={security_state:#010x}" + ); + assert_ne!(hw_rev_id, 0, "expected non-zero MCI hw_rev_id"); + + hw.boot().expect("hardware model failed to boot"); + hw.step_until_exit_success() + .expect("MCU firmware did not exit with PASS"); +} diff --git a/third_party/caliptra/caliptra-mcu-sw/BUILD.bazel b/third_party/caliptra/caliptra-mcu-sw/BUILD.bazel index fe65e636d..c935b451c 100644 --- a/third_party/caliptra/caliptra-mcu-sw/BUILD.bazel +++ b/third_party/caliptra/caliptra-mcu-sw/BUILD.bazel @@ -459,7 +459,7 @@ _MCU_ROM_RUSTC_FLAGS = [ ] # NOTE: This target requires the caliptra RISC-V platform transition to build. -# Build via //target/veer/tooling:mcu_rom_bin_transitioned instead of directly. +# Build via //target/veer/tooling:mcu_rom_{emulator,fpga} instead of directly. rust_binary( name = "mcu_rom", srcs = ["@caliptra_mcu_sw//:all_files"], diff --git a/third_party/caliptra/uprev.py b/third_party/caliptra/uprev.py index d2d8ab54b..0030bd3cd 100644 --- a/third_party/caliptra/uprev.py +++ b/third_party/caliptra/uprev.py @@ -621,7 +621,7 @@ def _do_bump_transaction( _info(f"caliptra_cfi: {old_cfi} -> {new_cfi} (Cargo.toml)") _info( " NOTE: run 'CARGO_BAZEL_REPIN=1 bazel build " - "//target/veer/tooling:caliptra_runner' to regenerate Cargo.lock" + "//target/veer/...' to regenerate Cargo.lock" ) else: _info(f"caliptra_cfi: already at {new_cfi} (no change)") @@ -678,7 +678,7 @@ def _do_bump_transaction( print(" 1. Check if overlay BUILD files need updating") print(" (upstream may have added/removed source files)") print(" 2. Verify the build:") - print(" bazel build //target/veer/tooling:caliptra_runner") + print(" bazel test //target/veer/...") print(" 3. Run tests and commit the changes") print("============================================================") return 0 diff --git a/workflows.json b/workflows.json index da747a8e9..f80641417 100644 --- a/workflows.json +++ b/workflows.json @@ -61,8 +61,8 @@ "build_type": "bazel", "args": [ "--keep_going", - "--build_tag_filters=-hardware,-disabled,-verilator,-emulator,-qemu", - "--test_tag_filters=-hardware,-disabled,-verilator,-emulator,-qemu", + "--build_tag_filters=-hardware,-disabled,-verilator,-emulator,-qemu,-fpga", + "--test_tag_filters=-hardware,-disabled,-verilator,-emulator,-qemu,-fpga", "--test_output=streamed", "--build_event_json_file=${BUILD_ROOT}/pw_ci_tests_bep.json" ], @@ -122,12 +122,12 @@ "name": "caliptra_fpga_build", "build_config": { "name": "caliptra_fpga_build_config", - "description": "Build (do not run) the Caliptra MCU fpga target image. Catches fpga cfg/build-wiring regressions without requiring lab hardware; actually running it needs a VCK190 board (see //target/veer/unittest_runner:fpga_test, tagged manual).", + "description": "Build (do not run) the Caliptra MCU FPGA test binaries and images. Catches FPGA cfg, cross-compile, and build-wiring regressions without requiring lab hardware.", "build_type": "bazel", "args": [ "--keep_going", - "--config=k_veer", - "--//target/veer:target_type=fpga" + "--config=fpga", + "--build_tag_filters=+fpga,-disabled" ], "driver_options": { "@type": "pw.build.proto.BazelDriverOptions", @@ -135,7 +135,7 @@ } }, "targets": [ - "//target/veer/unittest_runner:fpga_test" + "//target/veer/..." ] }, {