Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions .github/workflows/module_qemu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: module_qemu

# Copyright (c) 2026 Eclipse ThreadX contributors
# SPDX-License-Identifier: MIT
# Portions of this file were generated with AI assistance.
#
# Builds the module execution harness in test/tx/cmake/module_qemu for each
# Cortex-M module port it covers and EXECUTES every image on QEMU's MPS2
# board for that core, asserting each image's self-reported result.
#
# Why this exists: until this workflow landed, no Cortex-M module port ran in
# CI. gcc_check compiles the module manager sources once per port and links
# nothing that loads a module; the host tests under test/tx/module_manager
# compile a handful of portable module manager sources against stubs. A
# module's start-up, its kernel calls through the supervisor call, the MPU
# programming on every context switch and the fault path that terminates a
# misbehaving module are only exercised by executing them, and r52_fvp does
# that for the Cortex-R52 alone.
#
# What it covers, per port:
#
# - cortex_m3 on mps2-an385 and cortex_m4 on mps2-an386, the latter built
# for its FPU with the hard-float ABI;
# - a protected module loaded, started, exercising the kernel, stopped and
# unloaded twice, with its memory given back;
# - a protected module faulting on a read of kernel memory, reported to the
# application and terminated -- which is also the proof that QEMU's MPU is
# enforcing at all;
# - the same module built privileged and unprotected, whose identical read
# succeeds.
#
# QEMU comes from the Ubuntu archive. The images report through semihosting
# and end the simulation themselves, so a hang shows up as the runner's
# timeout rather than as a pass.

on:
# push as well as pull_request, for the reason gcc_check.yml states at
# length: dev needs its own baseline so a bad squash-merge is caught then
# rather than by whoever opens the next pull request.
push:
branches: [ master, dev ]
paths:
- ".github/workflows/module_qemu.yml"
- "common/**"
- "common_modules/**"
- "ports_module/cortex_m3/**"
- "ports_module/cortex_m4/**"
- "test/tx/cmake/module_qemu/**"
- "scripts/tx_ci_common.sh"
pull_request:
branches: [ master, dev ]
# Duplicated rather than shared through a YAML anchor, as in r52_fvp.yml.
# **Edit both.**
paths:
- ".github/workflows/module_qemu.yml"
- "common/**"
- "common_modules/**"
- "ports_module/cortex_m3/**"
- "ports_module/cortex_m4/**"
- "test/tx/cmake/module_qemu/**"
- "scripts/tx_ci_common.sh"

permissions:
contents: read

jobs:
module_qemu:
runs-on: ubuntu-24.04
timeout-minutes: 30

env:
# Pinned deliberately and shared with gcc_check.yml, ci_cortex_m.yml and
# r52_fvp.yml through one cache entry; change them together.
GCC_VERSION: 14.3.rel1

steps:
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Path and key match gcc_check.yml's AArch32 entry exactly, so the
# workflows share one cache entry.
- name: Cache the AArch32 Arm GNU toolchain
id: cache-arm32
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: toolchain
key: arm-gnu-toolchain-${{ env.GCC_VERSION }}-x86_64-arm-none-eabi

# The checksum suffix is .sha256asc and not .sha256; see gcc_check.yml.
- name: Install the AArch32 Arm GNU toolchain
if: steps.cache-arm32.outputs.cache-hit != 'true'
run: |
set -eu
base="https://developer.arm.com/-/media/Files/downloads/gnu/${GCC_VERSION}/binrel"
archive="arm-gnu-toolchain-${GCC_VERSION}-x86_64-arm-none-eabi.tar.xz"
mkdir -p toolchain && cd toolchain
curl -fsSLO "$base/$archive"
curl -fsSLO "$base/$archive.sha256asc"
sha256sum -c "$archive.sha256asc"
tar xf "$archive"
rm -f "$archive"

# Through the bounded, retried apt helpers in tx_ci_common.sh, for the
# reasons that file records: on this runner pool a bare apt call can stall
# for hours. Ninja is installed only if the image stopped shipping it.
- name: Install QEMU
run: |
set -eu
. scripts/tx_ci_common.sh
packages="qemu-system-arm"
command -v ninja >/dev/null 2>&1 || packages="$packages ninja-build"
retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" update || true
# shellcheck disable=SC2086
retry sudo "${TIMEOUT[@]}" apt-get "${APT_OPTIONS[@]}" install -y --no-install-recommends $packages
qemu-system-arm --version | head -1

- name: Build and run the module harness
run: |
set -eu
export PATH="$GITHUB_WORKSPACE/toolchain/arm-gnu-toolchain-${GCC_VERSION}-x86_64-arm-none-eabi/bin:$PATH"
arm-none-eabi-gcc --version | head -1

status=0
for port in cortex_m3 cortex_m4; do
build="build/module_qemu_$port"
echo ""
echo "=============================================================="
echo "== $port"
echo "=============================================================="

if ! cmake -S test/tx/cmake/module_qemu -B "$build" -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$PWD/test/tx/cmake/module_qemu/arm-none-eabi-qemu.cmake" \
-DMODULE_QEMU_PORT="$port"; then
echo "::error::$port: CMake configure failed"
status=1
continue
fi
if ! cmake --build "$build"; then
echo "::error::$port: the images did not build"
status=1
continue
fi
# --no-tests=error: the tests are registered only when QEMU and
# Python are found, and ctest's default would pass on none.
if ! ctest --test-dir "$build" --output-on-failure --no-tests=error; then
echo "::error::$port: one or more images failed on QEMU, or no tests were registered"
status=1
fi
done

exit $status
224 changes: 224 additions & 0 deletions test/tx/cmake/module_qemu/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Copyright (c) 2026 Eclipse ThreadX contributors
#
# This program and the accompanying materials are made available under the
# terms of the MIT License which is available at
# https://opensource.org/licenses/MIT.
#
# AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5.5).
# The AI-generated portions may be considered public domain (CC0-1.0)
# and not subject to the project's licence. The human contributor has
# reviewed and verified that the code is correct.
#
# SPDX-License-Identifier: MIT and CC0-1.0

# Module execution harness.
#
# Builds a module manager image per test, each embedding the modules it loads,
# for one Cortex-M module port, and runs every image on QEMU's MPS2 board for
# that core, asserting the result each image reports over semihosting.
#
# Why this exists: the host tests under test/tx/module_manager compile a few
# module manager sources against stubs, and nothing else in CI runs a Cortex-M
# module port at all. A module's start-up, its kernel calls through the
# supervisor call, the MPU programming on every context switch and the fault
# path that terminates a misbehaving module are only exercised by executing
# them. This is the per-architecture part of the module test strategy; the
# portable module manager code is meant to be covered on the host.
#
# Its own top-level project, like test/tx/cmake/riscv, because it cross-builds
# with a bare-metal toolchain and runs under an emulator, which the host suite
# beside it does neither of.
#
# Configure once per port:
#
# cmake -S test/tx/cmake/module_qemu -B build/module_qemu_m3 -G Ninja \
# -DCMAKE_TOOLCHAIN_FILE=$PWD/test/tx/cmake/module_qemu/arm-none-eabi-qemu.cmake \
# -DMODULE_QEMU_PORT=cortex_m3
# cmake --build build/module_qemu_m3
# ctest --test-dir build/module_qemu_m3 --output-on-failure
#
# MODULE_QEMU_PORT is cortex_m3 (QEMU mps2-an385) or cortex_m4 (mps2-an386,
# built for its FPU with the hard-float ABI).

cmake_minimum_required(VERSION 3.20 FATAL_ERROR)

project(threadx_module_qemu LANGUAGES C ASM)

set(MODULE_QEMU_PORT "cortex_m3" CACHE STRING "Cortex-M module port to build and run")
set_property(CACHE MODULE_QEMU_PORT PROPERTY STRINGS cortex_m3 cortex_m4)

if(MODULE_QEMU_PORT STREQUAL "cortex_m3")
set(PORT_CPU_FLAGS -mcpu=cortex-m3 -mthumb)
set(QEMU_MACHINE mps2-an385)
elseif(MODULE_QEMU_PORT STREQUAL "cortex_m4")
set(PORT_CPU_FLAGS -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard)
set(QEMU_MACHINE mps2-an386)
else()
message(FATAL_ERROR "MODULE_QEMU_PORT must be cortex_m3 or cortex_m4, not '${MODULE_QEMU_PORT}'")
endif()

set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..)
set(PORT_DIR ${REPO_ROOT}/ports_module/${MODULE_QEMU_PORT}/gnu)
set(HARNESS_SRC ${CMAKE_CURRENT_LIST_DIR}/src)
set(HARNESS_TESTS ${CMAKE_CURRENT_LIST_DIR}/tests)

message(STATUS "Module port: ${MODULE_QEMU_PORT}, QEMU machine: ${QEMU_MACHINE}")

add_compile_options(${PORT_CPU_FLAGS} -g -O0 -ffunction-sections -fdata-sections)
add_link_options(${PORT_CPU_FLAGS})

# ---------------------------------------------------------------------------
# The kernel and the Module Manager, built with the module port's headers.
#
# One library, not the threadx library plus the manager: the module port's
# tx_port.h adds the owning module to the kernel's control blocks, so a kernel
# built against the base port and a manager built against this one would
# disagree about every object while linking without complaint. tx_misra.c is
# left out because nothing here builds with TX_MISRA_ENABLE.
# ---------------------------------------------------------------------------

file(GLOB TX_COMMON_SRC ${REPO_ROOT}/common/src/*.c)
list(FILTER TX_COMMON_SRC EXCLUDE REGEX ".*/tx_misra\\.c$")
file(GLOB TXM_PORT_SRC
${PORT_DIR}/module_manager/src/*.S
${PORT_DIR}/module_manager/src/*.s
${PORT_DIR}/module_manager/src/*.c)
file(GLOB TXM_MANAGER_SRC ${REPO_ROOT}/common_modules/module_manager/src/*.c)

add_library(threadx_module STATIC ${TX_COMMON_SRC} ${TXM_PORT_SRC} ${TXM_MANAGER_SRC})
target_include_directories(threadx_module PUBLIC
${PORT_DIR}/inc
${REPO_ROOT}/common/inc
${REPO_ROOT}/common_modules/inc
${REPO_ROOT}/common_modules/module_manager/inc)

# ---------------------------------------------------------------------------
# The module-side library: the API shims that run inside a module. It belongs
# to the module images only; linked into a manager it would redirect the
# manager's own service calls into a module.
#
# Position independent, as everything in a module must be:
# -fpic / -fpie data references go through the GOT
# -msingle-pic-base r9 is the GOT base, set by the manager's
# thread stack build
# -mno-pic-data-is-text-relative data is allocated from the manager's pool,
# not at a fixed offset from the code
# -fno-plt no procedure linkage table to populate
# ---------------------------------------------------------------------------

set(MODULE_PIC_FLAGS -fno-plt -mno-pic-data-is-text-relative -msingle-pic-base)

file(GLOB TXM_MODULE_LIB_SRC
${REPO_ROOT}/common_modules/module_lib/src/*.c
${PORT_DIR}/module_lib/src/*.c)

add_library(txm_module_lib STATIC ${TXM_MODULE_LIB_SRC})
target_include_directories(txm_module_lib PUBLIC
${PORT_DIR}/inc
${REPO_ROOT}/common/inc
${REPO_ROOT}/common_modules/inc)
target_compile_options(txm_module_lib PRIVATE -fpic ${MODULE_PIC_FLAGS})

# ---------------------------------------------------------------------------
# A test module: one source, built with a given preamble property word, linked
# with the port's module linker script, and reduced to the raw image a manager
# embeds. The script defines the code and data sizes the preamble reports.
# ---------------------------------------------------------------------------

function(module_qemu_add_module name source properties)
add_executable(${name}.elf
${HARNESS_SRC}/module_preamble.S
${PORT_DIR}/example_build/gcc_setup.s
${source})
target_include_directories(${name}.elf PRIVATE ${HARNESS_SRC})
target_compile_definitions(${name}.elf PRIVATE TEST_MODULE_PROPERTIES=${properties})
target_compile_options(${name}.elf PRIVATE -fpie ${MODULE_PIC_FLAGS})
target_link_libraries(${name}.elf PRIVATE txm_module_lib)
target_link_options(${name}.elf PRIVATE
-T${PORT_DIR}/example_build/sample_threadx_module.ld
-nostartfiles
-nostdlib
-Wl,-pie
-Wl,--no-dynamic-linker
-Wl,-e,_txm_module_thread_shell_entry
-Wl,--no-warn-rwx-segments
-Wl,-Map=${name}.map)
set_target_properties(${name}.elf PROPERTIES
LINK_DEPENDS ${PORT_DIR}/example_build/sample_threadx_module.ld)

set(bin ${CMAKE_CURRENT_BINARY_DIR}/${name}.bin)
add_custom_command(
OUTPUT ${bin}
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${name}.elf> ${bin}
DEPENDS ${name}.elf
COMMENT "Converting ${name} to a raw module image"
VERBATIM)
add_custom_target(${name}_bin DEPENDS ${bin})

# The blob that embeds the image in a manager, as bytes, under the module's
# name as its symbol.
set(BLOB_SYMBOL ${name})
set(BLOB_FILE ${bin})
configure_file(${HARNESS_SRC}/module_blob.S.in ${CMAKE_CURRENT_BINARY_DIR}/${name}_blob.S @ONLY)
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/${name}_blob.S PROPERTIES OBJECT_DEPENDS ${bin})
endfunction()

# GNU compiler ID in bits 31-24; bit 0 user mode, bit 1 memory protection,
# bit 2 shared memory access.
module_qemu_add_module(module_services_protected ${HARNESS_TESTS}/module_services.c 0x02000007)
module_qemu_add_module(module_services_privileged ${HARNESS_TESTS}/module_services.c 0x02000000)

# ---------------------------------------------------------------------------
# The manager images, one per test, and the CTest entries that run them.
# ---------------------------------------------------------------------------

find_package(Python3 COMPONENTS Interpreter)
find_program(QEMU_SYSTEM_ARM qemu-system-arm)

enable_testing()

function(module_qemu_add_test name test_source)
set(blobs ${ARGN})
set(blob_sources "")
foreach(blob IN LISTS blobs)
list(APPEND blob_sources ${CMAKE_CURRENT_BINARY_DIR}/${blob}_blob.S)
endforeach()

add_executable(${name}.elf
${HARNESS_SRC}/startup.S
${HARNESS_SRC}/tx_initialize_low_level.S
${HARNESS_SRC}/harness.c
${HARNESS_TESTS}/harness_checks.c
${test_source}
${blob_sources})
target_include_directories(${name}.elf PRIVATE ${HARNESS_SRC} ${HARNESS_TESTS})
target_link_libraries(${name}.elf PRIVATE threadx_module)
target_link_options(${name}.elf PRIVATE
-T${HARNESS_SRC}/manager.ld
-nostartfiles
--specs=nano.specs
--specs=nosys.specs
-Wl,--gc-sections
-Wl,--no-warn-rwx-segments
-Wl,-Map=${name}.map)
set_target_properties(${name}.elf PROPERTIES LINK_DEPENDS ${HARNESS_SRC}/manager.ld)
foreach(blob IN LISTS blobs)
add_dependencies(${name}.elf ${blob}_bin)
endforeach()

if(Python3_FOUND AND QEMU_SYSTEM_ARM)
add_test(NAME ${MODULE_QEMU_PORT}::${name}
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/run_qemu_test.py
--qemu ${QEMU_SYSTEM_ARM}
--machine ${QEMU_MACHINE}
--elf $<TARGET_FILE:${name}.elf>)
endif()
endfunction()

module_qemu_add_test(test_module_lifecycle ${HARNESS_TESTS}/test_module_lifecycle.c module_services_protected)
module_qemu_add_test(test_module_fault ${HARNESS_TESTS}/test_module_fault.c module_services_protected)
module_qemu_add_test(test_module_privileged ${HARNESS_TESTS}/test_module_privileged.c module_services_privileged)

if(NOT (Python3_FOUND AND QEMU_SYSTEM_ARM))
message(STATUS "qemu-system-arm or Python 3 not found; the images build but no tests are registered.")
endif()
Loading