From b0c50fb71eeee987067df299c5b0f9585c03f6ca Mon Sep 17 00:00:00 2001 From: yan hu Date: Fri, 7 Aug 2026 18:05:09 +0800 Subject: [PATCH] [rust][tools] Fix Rust build example configuration handling Improve the Rust build integration across core, applications, components, and module examples. Fix build failure propagation, target and feature handling, linker flags, example configuration semantics, and add STM32 CI attach coverage. --- .../.ci/attachconfig/ci.attachconfig.yml | 19 ++ components/rust/SConscript | 2 +- components/rust/core/Cargo.toml | 11 + components/rust/core/SConscript | 86 ++++-- components/rust/core/src/api/mod.rs | 2 + components/rust/core/src/api/queue.rs | 11 +- components/rust/core/src/bindings/librt.rs | 3 +- components/rust/core/src/bindings/mod.rs | 3 +- components/rust/core/src/fs.rs | 2 +- components/rust/core/src/lib.rs | 3 +- .../rust/examples/application/SConscript | 22 +- .../rust/examples/application/fs/Cargo.toml | 4 +- .../examples/application/loadlib/Cargo.toml | 4 +- components/rust/examples/component/SConscript | 25 +- components/rust/examples/modules/SConscript | 37 ++- .../examples/modules/example_lib/Cargo.toml | 2 +- .../examples/modules/example_lib/src/lib.rs | 4 +- components/rust/tools/build_component.py | 218 ++++++++++++-- components/rust/tools/build_support.py | 199 ++++++++++--- components/rust/tools/build_usrapp.py | 269 +++++++++++++++--- .../rust/tools/feature_config_component.py | 1 + tools/requirements.txt | 3 +- 22 files changed, 758 insertions(+), 172 deletions(-) diff --git a/bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml b/bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml index a2bddc743c60..3817fd9f926c 100644 --- a/bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml +++ b/bsp/stm32/stm32f407-rt-spark/.ci/attachconfig/ci.attachconfig.yml @@ -246,6 +246,25 @@ component.cherryusb_cdc: - CONFIG_RT_CHERRYUSB_DEVICE_DWC2_ST=y - CONFIG_RT_CHERRYUSB_DEVICE_CDC_ACM=y - CONFIG_RT_CHERRYUSB_DEVICE_TEMPLATE_CDC_ACM=y +# ------ rust CI ------ +rust: + <<: *scons + pre_build: | + python3 -m pip install --user toml + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly --profile minimal + sudo ln -sf "$HOME/.cargo/bin/cargo" /usr/local/bin/cargo + sudo ln -sf "$HOME/.cargo/bin/rustc" /usr/local/bin/rustc + sudo ln -sf "$HOME/.cargo/bin/rustup" /usr/local/bin/rustup + rustup target add thumbv7em-none-eabihf + rustc --version + cargo --version + kconfig: + - CONFIG_RT_USING_RUST=y + - CONFIG_RT_RUST_CORE=y + - CONFIG_RT_USING_RUST_EXAMPLES=y + - CONFIG_RT_RUST_BUILD_APPLICATIONS=y + - CONFIG_RT_RUST_BUILD_COMPONENTS=y + - CONFIG_RUST_LOG_COMPONENT=y devices.soft_i2c: <<: *scons diff --git a/components/rust/SConscript b/components/rust/SConscript index ab84bd7a1444..97b40ad6c59c 100644 --- a/components/rust/SConscript +++ b/components/rust/SConscript @@ -10,7 +10,7 @@ def _has(sym: str) -> bool: except Exception: return bool(GetDepend(sym)) -if not _has('RT_USING_RUST'): +if not _has('RT_USING_RUST') and not GetOption('clean'): Return('objs') cwd = GetCurrentDir() diff --git a/components/rust/core/Cargo.toml b/components/rust/core/Cargo.toml index 4a4a0687df41..482b307b2314 100644 --- a/components/rust/core/Cargo.toml +++ b/components/rust/core/Cargo.toml @@ -10,6 +10,17 @@ crate-type = ["rlib", "staticlib"] [features] default = [] smp = [] +fs = [] +libdl = [] + +[package.metadata.rt-thread.features.smp] +all = ["RT_USING_SMP"] + +[package.metadata.rt-thread.features.fs] +all = ["RT_USING_DFS", "DFS_USING_POSIX"] + +[package.metadata.rt-thread.features.libdl] +all = ["RT_USING_MODULE"] [profile.dev] panic = "abort" diff --git a/components/rust/core/SConscript b/components/rust/core/SConscript index 880f6bce92c5..0fff31a5dadc 100644 --- a/components/rust/core/SConscript +++ b/components/rust/core/SConscript @@ -1,5 +1,7 @@ import os +import sys from building import * +from SCons.Subst import quote_spaces cwd = GetCurrentDir() @@ -11,6 +13,7 @@ from build_support import ( verify_rust_toolchain, ensure_rust_target_installed, cargo_build_staticlib, + get_staticlib_link_name, clean_rust_build, ) def _has(sym: str) -> bool: @@ -20,10 +23,28 @@ def _has(sym: str) -> bool: return bool(GetDepend(sym)) -# Source files – MSH command glue -src = ['rust_cmd.c'] +group = [] +if not _has('RT_RUST_CORE') and not GetOption('clean'): + Return('group') + + +def get_staticlib_link_name_from_artifact(lib_path): + """Derive the link name from the actual staticlib artifact file name.""" + artifact_name = os.path.basename(os.fspath(lib_path)) + if artifact_name.startswith("lib") and artifact_name.endswith(".a"): + return artifact_name[3:-2] + return None + + +# Source files – MSH command glue. +# rust_cmd.c references rust_init(), which is only provided by the Rust core +# static library. It must only enter the build when that library is actually +# produced; otherwise the C link stage fails with 'undefined reference to +# rust_init' instead of failing/skipping cleanly at the Rust build step. LIBS = [] LIBPATH = [] +LINKFLAGS = "" +include_rust_cmd = False if GetOption('clean'): # Register Rust artifacts for cleaning @@ -33,39 +54,64 @@ if GetOption('clean'): Clean('.', rust_build_dir) else: print('No rust build artifacts to clean') + # Keep rust_cmd.c in the group during clean so its object is cleaned too. + include_rust_cmd = True else: if verify_rust_toolchain(): import rtconfig + rust_build_dir = clean_rust_build(Dir('#').abspath) target = detect_rust_target(_has, rtconfig) if not target: print('Error: Unable to detect Rust target; please check configuration') + sys.exit(1) else: print(f'Detected Rust target: {target}') # Optional hint if target missing - ensure_rust_target_installed(target) - - # Build mode and features - debug = bool(_has('RUST_DEBUG_BUILD')) - features = collect_features(_has) + if not ensure_rust_target_installed(target): + print('Error: Rust target is not installed; Rust library build failed') + sys.exit(1) + else: + # Build mode and features + debug = bool(_has('RUST_DEBUG_BUILD')) + features = collect_features(_has) - rustflags = make_rustflags(rtconfig, target) - rust_lib = cargo_build_staticlib( - rust_dir=cwd, target=target, features=features, debug=debug, rustflags=rustflags - ) + rustflags = make_rustflags(rtconfig, target) + rust_lib = cargo_build_staticlib( + rust_dir=cwd, target=target, features=features, debug=debug, rustflags=rustflags, build_root=rust_build_dir + ) - if rust_lib: - LIBS = ['rt_rust'] - LIBPATH = [os.path.dirname(rust_lib)] - print('Rust library linked successfully') - else: - print('Warning: Failed to build Rust library') + if rust_lib: + # Derive the link name from the actual artifact so it always + # matches the file that was built. Only fall back to + # re-parsing Cargo.toml when the artifact name does not + # follow the lib.a convention. + link_lib_name = get_staticlib_link_name_from_artifact(rust_lib) + if not link_lib_name: + link_lib_name = get_staticlib_link_name(cwd) + LIBS = [link_lib_name] + LIBPATH = [os.path.dirname(rust_lib)] + if rtconfig.PLATFORM == 'armclang': + if not (os.path.isfile(rust_lib) and os.path.getsize(rust_lib) > 0): + print(f'Error: ArmClang Rust core link requires a non-empty archive, but got: {rust_lib}') + sys.exit(1) + LINKFLAGS = " " + quote_spaces(os.fspath(rust_lib)) + LIBS = [] + LIBPATH = [] + include_rust_cmd = True + print('Rust library linked successfully') + else: + print('Error: Failed to build Rust library') + sys.exit(1) else: - print('Warning: Rust toolchain not found') + print('Error: Rust toolchain not found') print('Please install Rust from https://rustup.rs') + sys.exit(1) -# Define component group for SCons -group = DefineGroup('rust', src, depend=['RT_USING_RUST'], LIBS=LIBS, LIBPATH=LIBPATH) +# Only define the component group (with rust_cmd.c) when the Rust core static +# library was actually produced, so its rust_init() reference always resolves. +if include_rust_cmd: + group = DefineGroup('rust', ['rust_cmd.c'], depend=['RT_USING_RUST', 'RT_RUST_CORE'], LIBS=LIBS, LIBPATH=LIBPATH, LINKFLAGS=LINKFLAGS) Return('group') diff --git a/components/rust/core/src/api/mod.rs b/components/rust/core/src/api/mod.rs index 8fc2ba23012b..cf3d0bbc0cb0 100644 --- a/components/rust/core/src/api/mod.rs +++ b/components/rust/core/src/api/mod.rs @@ -14,6 +14,7 @@ pub mod thread; pub mod mutex; pub mod sem; pub mod queue; +#[cfg(feature = "libdl")] pub mod libloading; @@ -24,4 +25,5 @@ pub use thread::*; pub use mutex::*; pub use sem::*; pub use queue::*; +#[cfg(feature = "libdl")] pub use libloading::*; diff --git a/components/rust/core/src/api/queue.rs b/components/rust/core/src/api/queue.rs index 96665e47f234..8a77374b907e 100644 --- a/components/rust/core/src/api/queue.rs +++ b/components/rust/core/src/api/queue.rs @@ -20,7 +20,7 @@ pub type APIRawQueue = rt_mq_t; pub(crate) fn queue_create(name: &str, len: u64, message_size: u64) -> Option { let s = CString::new(name).unwrap(); let raw; - unsafe { raw = rt_mq_create(s.as_ptr(), message_size, len, 0) } + unsafe { raw = rt_mq_create(s.as_ptr(), message_size as rt_size_t, len as rt_size_t, 0) } if raw == ptr::null_mut() { None } else { @@ -35,7 +35,7 @@ pub(crate) fn queue_send_wait( msg_size: u64, tick: i32, ) -> RttCResult { - unsafe { rt_mq_send_wait(handle, msg, msg_size, tick).into() } + unsafe { rt_mq_send_wait(handle, msg, msg_size as rt_size_t, tick).into() } } #[inline] @@ -45,7 +45,12 @@ pub(crate) fn queue_receive_wait( msg_size: u64, tick: i32, ) -> RttCResult { - unsafe { rt_mq_recv(handle, msg, msg_size, tick).into() } + let ret = unsafe { rt_mq_recv(handle, msg, msg_size as rt_size_t, tick) }; + if ret >= 0 { + RttCResult::Ok + } else { + ret.into() + } } #[inline] diff --git a/components/rust/core/src/bindings/librt.rs b/components/rust/core/src/bindings/librt.rs index 3f16c014fdb7..a39ed07c2283 100644 --- a/components/rust/core/src/bindings/librt.rs +++ b/components/rust/core/src/bindings/librt.rs @@ -26,6 +26,7 @@ pub type rt_int32_t = c_int; pub type rt_uint8_t = c_uchar; pub type rt_tick_t = rt_uint32_t; pub type rt_size_t = rt_ubase_t; +pub type rt_ssize_t = rt_base_t; pub type rt_thread_t = *mut c_void; pub type rt_sem_t = *mut c_void; @@ -94,7 +95,7 @@ unsafe extern "C" { pub fn rt_mq_create(name: *const c_char, msg_size: rt_size_t, max_msgs: rt_size_t, flag: rt_uint8_t) -> rt_mq_t; pub fn rt_mq_send(mq: rt_mq_t, buffer: *const c_void, size: rt_size_t) -> rt_err_t; pub fn rt_mq_send_wait(mq: rt_mq_t, buffer: *const c_void, size: rt_size_t, timeout: rt_int32_t) -> rt_err_t; - pub fn rt_mq_recv(mq: rt_mq_t, buffer: *mut c_void, size: rt_size_t, timeout: rt_int32_t) -> rt_base_t; + pub fn rt_mq_recv(mq: rt_mq_t, buffer: *mut c_void, size: rt_size_t, timeout: rt_int32_t) -> rt_ssize_t; pub fn rt_mq_delete(mq: rt_mq_t) -> rt_err_t; pub fn rt_mq_detach(mq: rt_mq_t) -> rt_err_t; } diff --git a/components/rust/core/src/bindings/mod.rs b/components/rust/core/src/bindings/mod.rs index ae2610e9c8d3..cad651a1dd29 100644 --- a/components/rust/core/src/bindings/mod.rs +++ b/components/rust/core/src/bindings/mod.rs @@ -46,8 +46,7 @@ pub use librt::{ /* Memory management functions */ pub use librt::{ - rt_malloc, rt_free, rt_realloc, rt_calloc, rt_malloc_align, rt_free_align, - rt_safe_malloc, rt_safe_free + rt_malloc, rt_free, rt_realloc, rt_calloc, rt_malloc_align, rt_free_align }; /* Device management functions */ diff --git a/components/rust/core/src/fs.rs b/components/rust/core/src/fs.rs index 6ad02037abde..9a8d1afa8dfb 100644 --- a/components/rust/core/src/fs.rs +++ b/components/rust/core/src/fs.rs @@ -98,7 +98,7 @@ impl File { pub fn seek(&self, offset: i64) -> RTResult { let n = unsafe { libc::lseek(self.fd, offset as libc::off_t, libc::SEEK_SET) }; - if n < 0 { Err(FileSeekErr) } else { Ok(n) } + if n < 0 { Err(FileSeekErr) } else { Ok(n.into()) } } pub fn flush(&self) -> RTResult<()> { diff --git a/components/rust/core/src/lib.rs b/components/rust/core/src/lib.rs index 01e4b34102b0..0b94f81427cb 100644 --- a/components/rust/core/src/lib.rs +++ b/components/rust/core/src/lib.rs @@ -17,7 +17,6 @@ and device interfaces. Designed for embedded devices running RT-Thread. */ #![no_std] -#![feature(alloc_error_handler)] #![feature(linkage)] #![allow(dead_code)] @@ -33,6 +32,7 @@ pub mod init; pub mod allocator; pub mod mutex; pub mod out; +#[cfg(feature = "fs")] pub mod fs; pub mod panic; pub mod param; @@ -40,6 +40,7 @@ pub mod queue; pub mod sem; pub mod thread; pub mod time; +#[cfg(feature = "libdl")] pub mod libloader; mod prelude; diff --git a/components/rust/examples/application/SConscript b/components/rust/examples/application/SConscript index c6c4de3db830..fc50bc282560 100644 --- a/components/rust/examples/application/SConscript +++ b/components/rust/examples/application/SConscript @@ -6,7 +6,7 @@ cwd = GetCurrentDir() # Import usrapp build module and build support sys.path.append(os.path.join(cwd, '../../tools')) -from build_usrapp import build_example_usrapp +from build_usrapp import UserAppBuildError, build_example_usrapp from build_support import clean_rust_build @@ -31,7 +31,7 @@ def load_extended_feature_configs(): group = [] -if not _has('RT_RUST_BUILD_APPLICATIONS'): +if not (_has('RT_RUST_BUILD_APPLICATIONS') or _has('RT_RUST_BUILD_ALL_EXAMPLES')) and not GetOption('clean'): Return('group') # Load extended feature configurations @@ -51,12 +51,16 @@ if GetOption('clean'): print('No example_usrapp build artifacts to clean') else: import rtconfig - LIBS, LIBPATH, LINKFLAGS = build_example_usrapp( - cwd=cwd, - has_func=_has, - rtconfig=rtconfig, - build_root=os.path.join(Dir('#').abspath, "build", "example_usrapp") - ) + try: + LIBS, LIBPATH, LINKFLAGS = build_example_usrapp( + cwd=cwd, + has_func=_has, + rtconfig=rtconfig, + build_root=os.path.join(Dir('#').abspath, "build", "example_usrapp") + ) + except UserAppBuildError as e: + print(f'Error: {e}') + sys.exit(1) group = DefineGroup( 'example_usrapp', @@ -67,4 +71,4 @@ group = DefineGroup( LINKFLAGS=LINKFLAGS ) -Return('group') \ No newline at end of file +Return('group') diff --git a/components/rust/examples/application/fs/Cargo.toml b/components/rust/examples/application/fs/Cargo.toml index 2556b5f0a420..40c2e01ffa84 100644 --- a/components/rust/examples/application/fs/Cargo.toml +++ b/components/rust/examples/application/fs/Cargo.toml @@ -12,6 +12,6 @@ default = [] enable-log = ["em_component_log/enable-log"] [dependencies] -rt-rust = { path = "../../../core" } +rt-rust = { path = "../../../core", features = ["fs"] } rt_macros = { path = "../../../rt_macros" } -em_component_log = { path = "../../component/log"} \ No newline at end of file +em_component_log = { path = "../../component/log"} diff --git a/components/rust/examples/application/loadlib/Cargo.toml b/components/rust/examples/application/loadlib/Cargo.toml index 1e1065f91aae..66c7abfb91a5 100644 --- a/components/rust/examples/application/loadlib/Cargo.toml +++ b/components/rust/examples/application/loadlib/Cargo.toml @@ -8,5 +8,5 @@ name = "em_loadlib" crate-type = ["staticlib"] [dependencies] -rt-rust = { path = "../../../core" } -rt_macros = { path = "../../../rt_macros" } \ No newline at end of file +rt-rust = { path = "../../../core", features = ["libdl"] } +rt_macros = { path = "../../../rt_macros" } diff --git a/components/rust/examples/component/SConscript b/components/rust/examples/component/SConscript index 40d11c044690..ccfa01673b50 100644 --- a/components/rust/examples/component/SConscript +++ b/components/rust/examples/component/SConscript @@ -6,7 +6,7 @@ cwd = GetCurrentDir() # Import component build module and build support sys.path.append(os.path.join(cwd, '../../tools')) -from build_component import build_example_component +from build_component import ComponentBuildError, build_example_component from build_support import clean_rust_build # Load feature configurations @@ -25,12 +25,11 @@ def _has(sym: str) -> bool: return bool(GetDepend(sym)) -# Early return if Rust or log component is not enabled -if not _has('RT_USING_RUST'): +if not _has('RT_USING_RUST') and not GetOption('clean'): group = [] Return('group') -if not _has('RUST_LOG_COMPONENT'): +if not (_has('RT_RUST_BUILD_COMPONENTS') or _has('RT_RUST_BUILD_ALL_EXAMPLES')) and not GetOption('clean'): group = [] Return('group') @@ -51,12 +50,16 @@ if GetOption('clean'): else: # Build the component using the extracted build module import rtconfig - LIBS, LIBPATH, LINKFLAGS = build_example_component( - cwd=cwd, - has_func=_has, - rtconfig=rtconfig, - build_root=os.path.join(Dir('#').abspath, "build", "example_component") - ) + try: + LIBS, LIBPATH, LINKFLAGS = build_example_component( + cwd=cwd, + has_func=_has, + rtconfig=rtconfig, + build_root=os.path.join(Dir('#').abspath, "build", "example_component") + ) + except ComponentBuildError as e: + print(f'Error: {e}') + sys.exit(1) # Define component group for SCons group = DefineGroup( @@ -68,4 +71,4 @@ group = DefineGroup( LINKFLAGS=LINKFLAGS ) -Return('group') \ No newline at end of file +Return('group') diff --git a/components/rust/examples/modules/SConscript b/components/rust/examples/modules/SConscript index 98e57d1fd9ca..056501c05799 100644 --- a/components/rust/examples/modules/SConscript +++ b/components/rust/examples/modules/SConscript @@ -32,6 +32,20 @@ def _has(sym: str) -> bool: except Exception: return bool(GetDepend(sym)) + +MODULE_CONFIG_MAP = { + 'example_lib': 'RT_RUST_MODULE_SIMPLE_MODULE', +} + + +def should_build_module(module_dir: str) -> bool: + if _has('RT_RUST_BUILD_ALL_EXAMPLES'): + return True + + config = MODULE_CONFIG_MAP.get(os.path.basename(module_dir)) + return bool(config and _has(config)) + + def detect_target_for_dynamic_modules(): """ Detect the appropriate Rust target for dynamic modules. @@ -68,7 +82,7 @@ def build_rust_module(module_dir, build_root): cargo_config = toml.load(f) module_name = cargo_config['package']['name'] - + # Detect target automatically based on the current configuration target = detect_target_for_dynamic_modules() print(f"Building Rust module '{module_name}' for target: {target}") @@ -121,7 +135,12 @@ def build_rust_module(module_dir, build_root): return [], [], "" # Check dependencies -if not _has('RT_RUST_BUILD_MODULES'): +if not _has('RT_USING_MODULE') and not GetOption('clean'): + Return([]) + +if not ( + _has('RT_RUST_BUILD_MODULES') or _has('RT_RUST_BUILD_ALL_EXAMPLES') +) and not GetOption('clean'): Return([]) build_root = os.path.join(Dir('#').abspath, "build", "rust_modules") @@ -142,7 +161,11 @@ else: modules_built = [] for item in os.listdir(cwd): item_path = os.path.join(cwd, item) - if os.path.isdir(item_path) and os.path.exists(os.path.join(item_path, 'Cargo.toml')): + if ( + os.path.isdir(item_path) + and os.path.exists(os.path.join(item_path, 'Cargo.toml')) + and should_build_module(item_path) + ): result = build_rust_module(item_path, build_root) if result[0]: modules_built.extend(result[0]) @@ -151,9 +174,9 @@ else: print(f"Successfully built {len(modules_built)} Rust dynamic module(s): {', '.join(modules_built)}") group = DefineGroup( - 'rust_modules', - [], - depend=['RT_RUST_BUILD_MODULES'] + 'rust_modules', + [], + depend=['RT_USING_RUST'] ) -Return('group') \ No newline at end of file +Return('group') diff --git a/components/rust/examples/modules/example_lib/Cargo.toml b/components/rust/examples/modules/example_lib/Cargo.toml index e44aae284886..6a490acad83f 100644 --- a/components/rust/examples/modules/example_lib/Cargo.toml +++ b/components/rust/examples/modules/example_lib/Cargo.toml @@ -9,4 +9,4 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -rt-rust = { path = "../../.." } \ No newline at end of file +rt-rust = { path = "../../../core" } \ No newline at end of file diff --git a/components/rust/examples/modules/example_lib/src/lib.rs b/components/rust/examples/modules/example_lib/src/lib.rs index 79fe384e6adf..8b86c593cd62 100644 --- a/components/rust/examples/modules/example_lib/src/lib.rs +++ b/components/rust/examples/modules/example_lib/src/lib.rs @@ -11,7 +11,7 @@ /* Bring rt-rust's println! macro into scope */ use rt_rust::println; use core::ffi::{c_char, CStr}; -#[no_mangle] +#[unsafe(no_mangle)] pub extern "C" fn rust_mylib_println(s: *const c_char) { if s.is_null() { println!(""); @@ -24,7 +24,7 @@ pub extern "C" fn rust_mylib_println(s: *const c_char) { } } -#[no_mangle] +#[unsafe(no_mangle)] pub extern "C" fn rust_mylib_add(a: usize, b: usize) -> usize { a + b } diff --git a/components/rust/tools/build_component.py b/components/rust/tools/build_component.py index 8b604f68b833..276958117b19 100644 --- a/components/rust/tools/build_component.py +++ b/components/rust/tools/build_component.py @@ -1,17 +1,126 @@ import os import subprocess +from SCons.Subst import quote_spaces # Configuration to feature mapping table for components # This table defines which RT-Thread configurations should enable which component features # All feature configurations are now defined in feature_config_component.py -CONFIG_COMPONENT_FEATURE_MAP = {} +CONFIG_COMPONENT_FEATURE_MAP = { +} class ComponentBuildError(Exception): pass +def load_toml_module(): + try: + import toml + return toml + except ImportError as e: + raise ComponentBuildError("Missing toml module required to parse component Cargo.toml") from e + + +def normalize_component_build_root(build_root, cwd): + """ + Normalize the component build root directory. + + Args: + build_root: Optional build root directory + cwd: Current working directory (component directory) + + Returns: + str: Absolute build root path + """ + if build_root is None: + if not cwd: + raise ComponentBuildError("Invalid build_root: cwd is required when build_root is None") + build_root = os.path.join(cwd, "build", "rust", "component") + + try: + build_root = os.fspath(build_root) + except TypeError: + raise ComponentBuildError("Invalid build_root: expected a non-empty path") + + if not isinstance(build_root, str) or not build_root: + raise ComponentBuildError("Invalid build_root: expected a non-empty path") + + return os.path.abspath(build_root) + + +def get_component_staticlib_artifact_name(rust_dir): + cargo_toml_path = os.path.join(rust_dir, "Cargo.toml") + lib_name = "em_component_registry" + + try: + import toml + with open(cargo_toml_path, "r") as f: + cargo_data = toml.load(f) + + package_name = cargo_data.get("package", {}).get("name") + lib_name = cargo_data.get("lib", {}).get("name") or package_name or lib_name + except Exception as e: + print(f"Warning: Failed to parse Rust component static library metadata from {cargo_toml_path}: {e}") + + if not isinstance(lib_name, str) or not lib_name: + lib_name = "em_component_registry" + + lib_name = lib_name.replace("-", "_") + return f"lib{lib_name}.a" + + +def get_component_staticlib_link_name(rust_dir): + cargo_toml_path = os.path.join(rust_dir, "Cargo.toml") + lib_name = "em_component_registry" + + try: + import toml + with open(cargo_toml_path, "r") as f: + cargo_data = toml.load(f) + + package_name = cargo_data.get("package", {}).get("name") + lib_name = cargo_data.get("lib", {}).get("name") or package_name or lib_name + except Exception as e: + print(f"Warning: Failed to parse Rust component static library metadata from {cargo_toml_path}: {e}") + + if not isinstance(lib_name, str) or not lib_name: + lib_name = "em_component_registry" + + return lib_name.replace("-", "_") + + +def get_staticlib_link_name_from_artifact(lib_path): + """ + Derive the linker library name from the actual staticlib artifact file name. + + Args: + lib_path: Path to the built staticlib artifact + + Returns: + str: Link name (artifact name without the leading 'lib' and trailing + '.a'), or None if the artifact does not follow that convention. + """ + artifact_name = os.path.basename(os.fspath(lib_path)) + if artifact_name.startswith("lib") and artifact_name.endswith(".a"): + return artifact_name[3:-2] + return None + + +def get_component_export_symbols(features): + enabled_features = set(features or []) + symbols = [] + for config_info in CONFIG_COMPONENT_FEATURE_MAP.values(): + if config_info['feature'] in enabled_features: + export_symbols = config_info.get('export_symbols', []) + if not export_symbols: + raise ComponentBuildError( + f"No link anchor metadata registered for enabled component feature '{config_info['feature']}'" + ) + symbols.extend(export_symbols) + return symbols + + def check_component_dependencies(component_dir, required_dependencies): """ Check if a component has the required dependencies @@ -65,7 +174,7 @@ def collect_component_features(has_func, component_dir=None): # Iterate through all configured mappings for config_name, config_info in CONFIG_COMPONENT_FEATURE_MAP.items(): # Check if this RT-Thread configuration is enabled - if has_func(config_name): + if has_func(config_name) or has_func('RT_RUST_BUILD_ALL_EXAMPLES'): feature_name = config_info['feature'] required_deps = config_info.get('dependencies', []) @@ -81,6 +190,26 @@ def collect_component_features(has_func, component_dir=None): return features + +def declared_component_features(component_dir): + cargo_toml_path = os.path.join(component_dir, 'Cargo.toml') + if not os.path.isfile(cargo_toml_path): + raise ComponentBuildError(f"Component Cargo.toml not found: {cargo_toml_path}") + + toml = load_toml_module() + try: + with open(cargo_toml_path, 'r') as f: + cargo_data = toml.load(f) + return set(cargo_data.get('features', {}).keys()) + except Exception as e: + raise ComponentBuildError(f"Failed to parse component features from {cargo_toml_path}: {e}") from e + + +def filter_declared_component_features(component_dir, features): + declared_features = declared_component_features(component_dir) + return [feature for feature in features if feature in declared_features] + + def cargo_build_component_staticlib(rust_dir, target, features, debug, rustflags=None, build_root=None): """ Build a Rust component as a static library using Cargo. @@ -91,15 +220,12 @@ def cargo_build_component_staticlib(rust_dir, target, features, debug, rustflags features: List of features to enable debug: Whether this is a debug build rustflags: Additional Rust compilation flags - build_root: Build root directory (if not provided, will raise error) + build_root: Build root directory Returns: str: Path to the built library file, or None if build failed """ - if not build_root: - raise ComponentBuildError("build_root parameter is required") - - build_root = os.path.abspath(build_root) + build_root = normalize_component_build_root(build_root, None) os.makedirs(build_root, exist_ok=True) env = os.environ.copy() @@ -121,10 +247,19 @@ def cargo_build_component_staticlib(rust_dir, target, features, debug, rustflags cmd += ["--no-default-features", "--features", ",".join(features)] print("Building example component log (cargo)…") - res = subprocess.run(cmd, cwd=rust_dir, env=env, capture_output=True, text=True) + try: + res = subprocess.run(cmd, cwd=rust_dir, env=env, capture_output=True, text=True) + except FileNotFoundError: + print("Error: cargo executable not found. Please install Rust/Cargo and ensure it is in PATH.") + return None if res.returncode != 0: - print("Warning: Example component build failed") + print(f"Warning: Example component build failed for {rust_dir}") + print(f"Target: {target}") + print(f"Command: {' '.join(cmd)}") + print(f"Return code: {res.returncode}") + if res.stdout: + print(res.stdout) if res.stderr: print(res.stderr) return None @@ -132,12 +267,13 @@ def cargo_build_component_staticlib(rust_dir, target, features, debug, rustflags mode = "debug" if debug else "release" # Try target-specific path first, then fallback to direct path - lib_path = os.path.join(build_root, target, mode, "libem_component_registry.a") - if os.path.exists(lib_path): + artifact_name = get_component_staticlib_artifact_name(rust_dir) + lib_path = os.path.join(build_root, target, mode, artifact_name) + if os.path.isfile(lib_path) and os.path.getsize(lib_path) > 0: print("Example component log built successfully") return lib_path - print("Warning: Library not found at expected location") + print("Warning: Rust component static library artifact not found, is not a file, or is empty") print(f"Expected: {lib_path}") return None @@ -157,26 +293,37 @@ def build_example_component(cwd, has_func, rtconfig, build_root=None): """ LIBS = [] LIBPATH = [] - LINKFLAGS = "" - + LINKFLAGS = [] + # Import build support functions import sys - sys.path.append(os.path.join(cwd, '../rust/tools')) + tools_dir = os.path.abspath(os.path.join(cwd, '..', '..', 'tools')) + if tools_dir not in sys.path: + sys.path.append(tools_dir) from build_support import ( detect_rust_target, + ensure_rust_target_installed, + collect_features, make_rustflags, ) target = detect_rust_target(has_func, rtconfig) + if not target: + raise ComponentBuildError(f'Could not detect Rust target for example component build in {cwd}') # Build mode and features debug = bool(has_func('RUST_DEBUG_BUILD')) + features = collect_features(has_func) # Build the component registry registry_dir = os.path.join(cwd, 'component_registry') - features = collect_component_features(has_func, registry_dir) + features += collect_component_features(has_func, registry_dir) + features = filter_declared_component_features(registry_dir, features) rustflags = make_rustflags(rtconfig, target) + build_root = normalize_component_build_root(build_root, cwd) + if not ensure_rust_target_installed(target): + raise ComponentBuildError(f"Rust target '{target}' is not installed; example component build failed") rust_lib = cargo_build_component_staticlib( rust_dir=registry_dir, @@ -188,13 +335,38 @@ def build_example_component(cwd, has_func, rtconfig, build_root=None): ) if rust_lib: - LIBS = ['em_component_registry'] - LIBPATH = [os.path.dirname(rust_lib)] - # Add LINKFLAGS to ensure component is linked into final binary - LINKFLAGS += " -Wl,--whole-archive -lem_component_registry -Wl,--no-whole-archive" - LINKFLAGS += " -Wl,--allow-multiple-definition" + lib_dir = os.path.dirname(rust_lib) + # Derive the link name from the actual artifact so it always matches + # the file that was built. Only fall back to re-parsing Cargo.toml when + # the artifact name does not follow the lib.a convention. + link_lib_name = get_staticlib_link_name_from_artifact(rust_lib) + if not link_lib_name: + link_lib_name = get_component_staticlib_link_name(registry_dir) + LIBS = [link_lib_name] + LIBPATH = [lib_dir] + platform = getattr(rtconfig, 'PLATFORM', None) + if platform == 'armclang': + if not (os.path.isfile(rust_lib) and os.path.getsize(rust_lib) > 0): + raise ComponentBuildError( + f"ArmClang Rust component link requires a non-empty archive, but got: {rust_lib}" + ) + anchors = get_component_export_symbols(features) + LINKFLAGS = [f"--undefined={symbol}" for symbol in anchors] + LINKFLAGS.append(quote_spaces(os.fspath(rust_lib))) + LINKFLAGS = " " + " ".join(LINKFLAGS) + LIBS = [] + LIBPATH = [] + else: + LINKFLAGS = [ + quote_spaces(f"-L{os.fspath(lib_dir)}"), + "-Wl,--whole-archive", + f"-l{link_lib_name}", + "-Wl,--no-whole-archive", + "-Wl,--allow-multiple-definition", + ] + LINKFLAGS = " " + " ".join(LINKFLAGS) print('Example component registry library linked successfully') else: - print('Warning: Failed to build example component registry library') + raise ComponentBuildError(f"Failed to build example component registry library in {registry_dir} for target {target}") - return LIBS, LIBPATH, LINKFLAGS \ No newline at end of file + return LIBS, LIBPATH, LINKFLAGS diff --git a/components/rust/tools/build_support.py b/components/rust/tools/build_support.py index eb05fc1f2990..2f0c30dc7b43 100644 --- a/components/rust/tools/build_support.py +++ b/components/rust/tools/build_support.py @@ -48,10 +48,14 @@ def detect_rust_target(has, rtconfig): cflags = getattr(rtconfig, "CFLAGS", "") hard_float = "-mfloat-abi=hard" in cflags or has("ARCH_ARM_FPU") or has("ARCH_FPU_VFP") + if has("ARCH_ARM_CORTEX_M0") or has("ARCH_ARM_CORTEX_M0PLUS"): + return "thumbv6m-none-eabi" if has("ARCH_ARM_CORTEX_M3"): return "thumbv7m-none-eabi" if has("ARCH_ARM_CORTEX_M4") or has("ARCH_ARM_CORTEX_M7"): return "thumbv7em-none-eabihf" if hard_float else "thumbv7em-none-eabi" + if has("ARCH_ARM_CORTEX_M23"): + return "thumbv8m.base-none-eabi" if has("ARCH_ARM_CORTEX_M33"): # v8m.main return "thumbv8m.main-none-eabi" @@ -93,8 +97,17 @@ def detect_rust_target(has, rtconfig): return "armv7a-none-eabi" if "riscv32" in arch_l: return "riscv32imac-unknown-none-elf" - if "riscv64" in arch_l or "risc-v" in arch_l: - # Many BSPs use "risc-v" token; assume 64-bit for virt64 + if "riscv64" in arch_l: + return "riscv64imac-unknown-none-elf" + if "risc-v" in arch_l: + info = _parse_cflags(getattr(rtconfig, "CFLAGS", "")) + abi = info["mabi"] or "" + abi_has_fp = abi.endswith("f") or abi.endswith("d") + if info["rv_bits"] == 32: + return "riscv32imafc-unknown-none-elf" if abi_has_fp else "riscv32imac-unknown-none-elf" + if info["rv_bits"] == 64: + return "riscv64gc-unknown-none-elf" if abi_has_fp else "riscv64imac-unknown-none-elf" + # Many BSPs use "risc-v" token; assume 64-bit for virt64 when CFLAGS do not specify width return "riscv64imac-unknown-none-elf" # Parse CFLAGS for hints @@ -106,35 +119,15 @@ def detect_rust_target(has, rtconfig): return "thumbv7em-none-eabihf" return "thumbv7em-none-eabi" if "-march=rv32" in cflags: - march_val = None - mabi_val = None - for flag in cflags.split(): - if flag.startswith("-march="): - march_val = flag[len("-march="):] - elif flag.startswith("-mabi="): - mabi_val = flag[len("-mabi="):] - has_f_or_d = False - if march_val and any(x in march_val for x in ("f", "d")): - has_f_or_d = True - if mabi_val and any(x in mabi_val for x in ("f", "d")): - has_f_or_d = True - return "riscv32imafc-unknown-none-elf" if has_f_or_d else "riscv32imac-unknown-none-elf" + info = _parse_cflags(cflags) + abi = info["mabi"] or "" + abi_has_fp = abi.endswith("f") or abi.endswith("d") + return "riscv32imafc-unknown-none-elf" if abi_has_fp else "riscv32imac-unknown-none-elf" if "-march=rv64" in cflags: - march_val = None - mabi_val = None - for flag in cflags.split(): - if flag.startswith("-march="): - march_val = flag[len("-march="):] - elif flag.startswith("-mabi="): - mabi_val = flag[len("-mabi="):] - has_f_or_d = False - if mabi_val and (("lp64d" in mabi_val) or ("lp64f" in mabi_val)): - has_f_or_d = True - if march_val and any(x in march_val for x in ("f", "d")): - has_f_or_d = True - if mabi_val and any(x in mabi_val for x in ("f", "d")): - has_f_or_d = True - if has_f_or_d: + info = _parse_cflags(cflags) + abi = info["mabi"] or "" + abi_has_fp = abi.endswith("f") or abi.endswith("d") + if abi_has_fp: return "riscv64gc-unknown-none-elf" return "riscv64imac-unknown-none-elf" @@ -142,6 +135,14 @@ def detect_rust_target(has, rtconfig): def make_rustflags(rtconfig, target: str): + if not isinstance(target, str) or not target: + arch = getattr(rtconfig, "ARCH", None) + cflags = getattr(rtconfig, "CFLAGS", "") + raise ValueError( + "Unsupported Rust target: unable to detect a Rust target " + f"for ARCH={arch!r}, CFLAGS={cflags!r}" + ) + rustflags = [ "-C", "opt-level=z", "-C", "panic=abort", @@ -166,9 +167,69 @@ def make_rustflags(rtconfig, target: str): def collect_features(has): + cargo_toml_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "core", "Cargo.toml") + ) + if not os.path.isfile(cargo_toml_path): + raise RuntimeError(f"Rust core Cargo.toml not found: {cargo_toml_path}") + + try: + import toml + except ImportError as e: + raise RuntimeError("Missing toml module required to parse Rust core Cargo.toml") from e + + try: + with open(cargo_toml_path, "r") as f: + cargo_data = toml.load(f) + except OSError as e: + raise RuntimeError(f"Failed to read Rust core Cargo.toml {cargo_toml_path}: {e}") from e + except Exception as e: + raise RuntimeError(f"Failed to parse Rust core Cargo.toml {cargo_toml_path}: {e}") from e + + declared_features = cargo_data.get("features") + if not isinstance(declared_features, dict): + raise RuntimeError("Invalid Rust core Cargo.toml: [features] must be a table") + + package = cargo_data.get("package") + metadata = package.get("metadata") if isinstance(package, dict) else None + rt_thread = metadata.get("rt-thread") if isinstance(metadata, dict) else None + feature_mappings = rt_thread.get("features") if isinstance(rt_thread, dict) else None + if not isinstance(feature_mappings, dict): + raise RuntimeError( + "Invalid Rust core feature metadata: package.metadata.rt-thread.features must be a table" + ) + + for feature in feature_mappings: + if feature not in declared_features: + raise RuntimeError( + f"Rust core feature metadata references undeclared Cargo feature: {feature}" + ) + feats = [] - if has("RT_USING_SMP"): - feats.append("smp") + for feature in declared_features: + if feature == "default" or feature not in feature_mappings: + continue + + mapping = feature_mappings[feature] + if not isinstance(mapping, dict): + raise RuntimeError(f"Invalid Rust core feature metadata for '{feature}': expected a table") + if set(mapping) != {"all"}: + raise RuntimeError( + f"Invalid Rust core feature metadata for '{feature}': only 'all' is supported" + ) + + symbols = mapping["all"] + if not isinstance(symbols, list): + raise RuntimeError(f"Invalid Rust core feature metadata for '{feature}': 'all' must be a list") + if not symbols: + raise RuntimeError(f"Invalid Rust core feature metadata for '{feature}': 'all' must not be empty") + if any(not isinstance(symbol, str) or not symbol for symbol in symbols): + raise RuntimeError( + f"Invalid Rust core feature metadata for '{feature}': 'all' must contain only non-empty strings" + ) + + if all(has(symbol) for symbol in symbols): + feats.append(feature) return feats @@ -181,10 +242,60 @@ def verify_rust_toolchain(): return False +def parse_installed_rust_targets(output): + return {line.strip() for line in output.splitlines() if line.strip()} + + +def get_staticlib_artifact_name(rust_dir): + cargo_toml_path = os.path.join(rust_dir, "Cargo.toml") + lib_name = "rt_rust" + + try: + import toml + with open(cargo_toml_path, "r") as f: + cargo_data = toml.load(f) + + package_name = cargo_data.get("package", {}).get("name") + lib_name = cargo_data.get("lib", {}).get("name") or package_name or lib_name + except Exception as e: + print(f"Warning: Failed to parse Rust static library metadata from {cargo_toml_path}: {e}") + + if not isinstance(lib_name, str) or not lib_name: + lib_name = "rt_rust" + + lib_name = lib_name.replace("-", "_") + return f"lib{lib_name}.a" + + +def get_staticlib_link_name(rust_dir): + cargo_toml_path = os.path.join(rust_dir, "Cargo.toml") + lib_name = "rt_rust" + + try: + import toml + with open(cargo_toml_path, "r") as f: + cargo_data = toml.load(f) + + package_name = cargo_data.get("package", {}).get("name") + lib_name = cargo_data.get("lib", {}).get("name") or package_name or lib_name + except Exception as e: + print(f"Warning: Failed to parse Rust static library metadata from {cargo_toml_path}: {e}") + + if not isinstance(lib_name, str) or not lib_name: + lib_name = "rt_rust" + + return lib_name.replace("-", "_") + + def ensure_rust_target_installed(target: str): + if not isinstance(target, str) or not target: + print("Invalid Rust target: expected a non-empty string") + return False + try: result = subprocess.run(["rustup", "target", "list", "--installed"], capture_output=True, text=True) - if result.returncode == 0 and target in result.stdout: + installed_targets = parse_installed_rust_targets(result.stdout) + if result.returncode == 0 and target in installed_targets: return True print(f"Rust target '{target}' is not installed.") print(f"Please install it with: rustup target add {target}") @@ -193,8 +304,11 @@ def ensure_rust_target_installed(target: str): return False -def cargo_build_staticlib(rust_dir: str, target: str, features, debug: bool, rustflags: str = None): - build_root = os.path.join((os.path.abspath(os.path.join(rust_dir, os.pardir, os.pardir))), "build", "rust") +def cargo_build_staticlib(rust_dir: str, target: str, features, debug: bool, rustflags: str = None, build_root: str = None): + if build_root is None: + build_root = os.path.join((os.path.abspath(os.path.join(rust_dir, os.pardir, os.pardir))), "build", "rust") + else: + build_root = os.path.abspath(os.fspath(build_root)) target_dir = os.path.join(build_root, "target") os.makedirs(build_root, exist_ok=True) @@ -211,7 +325,11 @@ def cargo_build_staticlib(rust_dir: str, target: str, features, debug: bool, rus cmd += ["--no-default-features", "--features", ",".join(features)] print("Building Rust component (cargo)…") - res = subprocess.run(cmd, cwd=rust_dir, env=env, capture_output=True, text=True) + try: + res = subprocess.run(cmd, cwd=rust_dir, env=env, capture_output=True, text=True) + except FileNotFoundError: + print("Error: cargo executable not found. Please install Rust/Cargo and ensure it is in PATH.") + return None if res.returncode != 0: print("Warning: Rust build failed") if res.stderr: @@ -219,15 +337,16 @@ def cargo_build_staticlib(rust_dir: str, target: str, features, debug: bool, rus return None mode = "debug" if debug else "release" - lib_path = os.path.join(target_dir, target, mode, "librt_rust.a") - if os.path.exists(lib_path): + artifact_name = get_staticlib_artifact_name(rust_dir) + lib_path = os.path.join(target_dir, target, mode, artifact_name) + if os.path.isfile(lib_path) and os.path.getsize(lib_path) > 0: print("Rust component built successfully") return lib_path - print("Warning: Library not found at expected location") + print(f"Warning: Rust static library artifact not found, is not a file, or is empty: {lib_path}") return None def clean_rust_build(bsp_root: str, artifact_type: str = "rust"): """Return the build directory path for SCons Clean operation""" build_dir = os.path.join(bsp_root, "build", artifact_type) - return build_dir \ No newline at end of file + return build_dir diff --git a/components/rust/tools/build_usrapp.py b/components/rust/tools/build_usrapp.py index 9d97a60ffeb4..0d7d4dc5bfdd 100644 --- a/components/rust/tools/build_usrapp.py +++ b/components/rust/tools/build_usrapp.py @@ -1,7 +1,7 @@ import os import subprocess -import toml import shutil +from SCons.Subst import quote_spaces # Configuration to feature mapping table @@ -21,6 +21,35 @@ 'thread': 'RT_RUST_EXAMPLE_THREAD' } +APP_DEPENDENCY_MAP = { + 'loadlib': ['RT_USING_MODULE'], +} + +APP_EXPORT_SYMBOL_MAP = { + 'fs': ['__rust_file_demo_cmd_seg'], + 'loadlib': ['__rust_dl_demo_cmd_seg'], + 'mutex': ['__rust_mutex_demo_cmd_seg'], + 'param': ['__rust_param_demo_cmd_seg'], + 'queue': ['__rust_queue_demo_cmd_seg'], + 'semaphore': ['__rust_sem_demo_cmd_seg'], + 'thread': ['__rust_thread_demo_cmd_seg'], +} + + +def app_dependencies_satisfied(app_name, has_func): + if app_name == 'fs': + if has_func('RT_USING_POSIX_FS'): + return True + if not has_func('RT_USING_DFS'): + return False + return has_func('DFS_USING_POSIX') or has_func('RT_USING_DFS_V2') + + for dep in APP_DEPENDENCY_MAP.get(app_name, []): + if not has_func(dep): + return False + + return True + def should_build_app(app_dir, has_func): """ @@ -35,6 +64,12 @@ def should_build_app(app_dir, has_func): """ # Get the application name from the directory app_name = os.path.basename(app_dir) + + if not app_dependencies_satisfied(app_name, has_func): + return False + + if has_func('RT_RUST_BUILD_ALL_EXAMPLES'): + return True # Check if there's a specific Kconfig option for this app if app_name in APP_CONFIG_MAP: @@ -45,6 +80,16 @@ def should_build_app(app_dir, has_func): return has_func('RT_RUST_BUILD_APPLICATIONS') +def get_app_export_symbols(app_dir): + app_name = os.path.basename(app_dir) + if app_name not in APP_EXPORT_SYMBOL_MAP: + raise UserAppBuildError( + f"No link anchor metadata registered for enabled user app '{app_name}'. " + f"Path: {app_dir}. Add its export anchor to APP_EXPORT_SYMBOL_MAP." + ) + return list(APP_EXPORT_SYMBOL_MAP[app_name]) + + def check_app_dependencies(app_dir, required_dependencies): """ Check if an application has the required dependencies @@ -64,6 +109,7 @@ def check_app_dependencies(app_dir, required_dependencies): return False try: + toml = load_toml_module() with open(cargo_toml_path, 'r') as f: cargo_data = toml.load(f) @@ -81,6 +127,25 @@ def check_app_dependencies(app_dir, required_dependencies): return False +def app_has_feature_dependency(app_dir, feature_name, dependency_name): + cargo_toml_path = os.path.join(app_dir, 'Cargo.toml') + if not os.path.exists(cargo_toml_path): + return False + + try: + toml = load_toml_module() + with open(cargo_toml_path, 'r') as f: + cargo_data = toml.load(f) + + features = cargo_data.get('features', {}) + dependencies = cargo_data.get('dependencies', {}) + return feature_name in features and dependency_name in dependencies + + except Exception as e: + print(f"Warning: Failed to parse {cargo_toml_path}: {e}") + return False + + def collect_features(has_func, app_dir=None): """ Collect Rust features based on RT-Thread configuration using extensible mapping table @@ -110,6 +175,10 @@ def collect_features(has_func, app_dir=None): # If no app_dir provided, enable for all (backward compatibility) features.append(feature_name) print(f"Enabling feature '{feature_name}' for {config_name}") + + if app_dir and os.path.basename(app_dir) == 'fs': + if app_has_feature_dependency(app_dir, 'enable-log', 'em_component_log') and 'enable-log' not in features: + features.append('enable-log') return features @@ -122,6 +191,41 @@ class UserAppBuildError(Exception): pass +def load_toml_module(): + try: + import toml + return toml + except ImportError as e: + raise UserAppBuildError("Missing toml module required to parse Cargo.toml") from e + + +def normalize_build_root(build_root, cwd): + """ + Normalize the user application build root directory. + + Args: + build_root: Optional build root directory + cwd: Current working directory (usrapp directory) + + Returns: + str: Absolute build root path + """ + if build_root is None: + if not cwd: + raise UserAppBuildError("Invalid build_root: cwd is required when build_root is None") + build_root = os.path.join(cwd, "build", "rust", "usrapp") + + try: + build_root = os.fspath(build_root) + except TypeError: + raise UserAppBuildError("Invalid build_root: expected a non-empty path") + + if not isinstance(build_root, str) or not build_root: + raise UserAppBuildError("Invalid build_root: expected a non-empty path") + + return os.path.abspath(build_root) + + def parse_cargo_toml(cargo_toml_path): """ Parse Cargo.toml file to extract library name and library type @@ -133,6 +237,7 @@ def parse_cargo_toml(cargo_toml_path): tuple: (lib_name, is_staticlib) """ try: + toml = load_toml_module() with open(cargo_toml_path, 'r') as f: cargo_data = toml.load(f) @@ -166,14 +271,23 @@ def discover_user_apps(base_dir): user_apps = [] for root, dirs, files in os.walk(base_dir): + dirs[:] = [d for d in dirs if d not in ("build", "target")] if 'Cargo.toml' in files: - if 'target' in root or 'build' in root: - continue user_apps.append(root) return user_apps +def staticlib_candidates(lib_name): + normalized_name = lib_name.replace('-', '_') + candidates = [(f"lib{normalized_name}.a", normalized_name)] + + if normalized_name != lib_name: + candidates.append((f"lib{lib_name}.a", lib_name)) + + return candidates + + def build_user_app(app_dir, target, debug, rustflags, build_root, features=None): """ Build a single user application @@ -189,15 +303,24 @@ def build_user_app(app_dir, target, debug, rustflags, build_root, features=None) Returns: tuple: (success, lib_name, lib_path) """ + build_root = normalize_build_root(build_root, None) + try: cargo_toml_path = os.path.join(app_dir, 'Cargo.toml') lib_name, is_staticlib = parse_cargo_toml(cargo_toml_path) if not is_staticlib: - return False, None, None + raise UserAppBuildError(f"User app in {app_dir} is not configured as a staticlib") env = os.environ.copy() - env['RUSTFLAGS'] = rustflags + previous_rustflags = env.get('RUSTFLAGS', '').strip() + new_rustflags = rustflags.strip() if rustflags else '' + if previous_rustflags and new_rustflags: + env['RUSTFLAGS'] = f'{previous_rustflags} {new_rustflags}' + elif new_rustflags: + env['RUSTFLAGS'] = new_rustflags + elif previous_rustflags: + env['RUSTFLAGS'] = previous_rustflags env['CARGO_TARGET_DIR'] = build_root cmd = ['cargo', 'build', '--target', target] @@ -209,8 +332,12 @@ def build_user_app(app_dir, target, debug, rustflags, build_root, features=None) cmd.extend(['--features', ','.join(features)]) print(f"Building example user app {lib_name} (cargo)…") - result = subprocess.run(cmd, cwd=app_dir, env=env, - capture_output=True, text=True) + try: + result = subprocess.run(cmd, cwd=app_dir, env=env, + capture_output=True, text=True) + except FileNotFoundError: + print("Error: cargo executable not found. Please install Rust/Cargo and ensure it is in PATH.") + raise UserAppBuildError(f"Cargo executable not found while building user app in {app_dir}") if result.returncode != 0: print(f"Failed to build user app in {app_dir}") @@ -218,19 +345,20 @@ def build_user_app(app_dir, target, debug, rustflags, build_root, features=None) print(f"Return code: {result.returncode}") print(f"STDOUT: {result.stdout}") print(f"STDERR: {result.stderr}") - return False, None, None + raise UserAppBuildError(f"Failed to build user app in {app_dir}") - lib_file = find_library_file(build_root, target, lib_name, debug) + lib_file, link_lib_name = find_library_file(build_root, target, lib_name, debug) if lib_file: # Return the library name for linking - return True, lib_name, lib_file + return True, link_lib_name, lib_file else: print(f"Library file not found for lib {lib_name}") - return False, None, None + raise UserAppBuildError(f"Library file not found for user app {lib_name}") + except UserAppBuildError: + raise except Exception as e: - print(f"Exception occurred while building user app in {app_dir}: {e}") - return False, None, None + raise UserAppBuildError(f"Exception occurred while building user app in {app_dir}: {e}") from e def find_library_file(build_root, target, lib_name, debug): @@ -244,15 +372,11 @@ def find_library_file(build_root, target, lib_name, debug): debug: Whether this is a debug build Returns: - str: Library file path, or None if not found + tuple: (library file path, link library name), or (None, None) if not found """ + build_root = normalize_build_root(build_root, None) profile = "debug" if debug else "release" - - possible_names = [ - f"lib{lib_name}.a", - f"lib{lib_name.replace('-', '_')}.a" - ] - + search_paths = [ os.path.join(build_root, target, profile), os.path.join(build_root, target, profile, "deps") @@ -262,15 +386,17 @@ def find_library_file(build_root, target, lib_name, debug): if not os.path.exists(search_path): continue - for name in possible_names: + for name, link_lib_name in staticlib_candidates(lib_name): lib_path = os.path.join(search_path, name) if os.path.exists(lib_path): - return lib_path + if os.path.isfile(lib_path) and os.path.getsize(lib_path) > 0: + return lib_path, link_lib_name + print(f"Warning: Rust user app static library artifact not found, is not a file, or is empty: {lib_path}") - return None + return None, None -def build_all_user_apps(base_dir, target, debug, rustflags, build_root, has_func): +def build_all_user_apps(base_dir, target, debug, rustflags, build_root, has_func, require_export_symbols=False): """ Build all user applications @@ -287,10 +413,12 @@ def build_all_user_apps(base_dir, target, debug, rustflags, build_root, has_func """ LIBS = [] LIBPATH = [] + LIBFILES = [] + UNDEFINED_SYMBOLS = [] success_count = 0 + total_count = 0 user_apps = discover_user_apps(base_dir) - total_count = len(user_apps) for app_dir in user_apps: # Check if this application should be built based on Kconfig @@ -298,6 +426,8 @@ def build_all_user_apps(base_dir, target, debug, rustflags, build_root, has_func app_name = os.path.basename(app_dir) print(f"Skipping {app_name} (disabled in Kconfig)") continue + + total_count += 1 # Collect features for this specific app features = collect_features(has_func, app_dir) @@ -307,15 +437,20 @@ def build_all_user_apps(base_dir, target, debug, rustflags, build_root, has_func app_name = os.path.basename(app_dir) print(f"Example user app {app_name} built successfully") LIBS.append(lib_name) + LIBFILES.append(lib_path) + if require_export_symbols: + UNDEFINED_SYMBOLS.extend(get_app_export_symbols(app_dir)) lib_dir = os.path.dirname(lib_path) if lib_dir not in LIBPATH: LIBPATH.append(lib_dir) success_count += 1 + else: + raise UserAppBuildError(f"Failed to build enabled user app: {app_dir}") - return LIBS, LIBPATH, success_count, total_count + return LIBS, LIBPATH, LIBFILES, UNDEFINED_SYMBOLS, success_count, total_count -def generate_linkflags(LIBS, LIBPATH): +def generate_linkflags(LIBS, LIBPATH, platform, LIBFILES=None, UNDEFINED_SYMBOLS=None): """ Generate link flags @@ -326,15 +461,39 @@ def generate_linkflags(LIBS, LIBPATH): Returns: str: Link flags string """ + if platform == 'armclang': + if not LIBFILES: + raise UserAppBuildError( + "ArmClang Rust link requires built static library archives, but none were produced" + ) + for lib in LIBFILES: + if not (os.path.isfile(lib) and os.path.getsize(lib) > 0): + raise UserAppBuildError( + f"ArmClang Rust link requires a non-empty archive, but got: {lib}" + ) + if not UNDEFINED_SYMBOLS: + raise UserAppBuildError( + "ArmClang Rust link requires at least one --undefined anchor symbol, but none were resolved" + ) + linkflags = [f"--undefined={symbol}" for symbol in UNDEFINED_SYMBOLS] + linkflags.extend(quote_spaces(os.fspath(lib)) for lib in LIBFILES) + return " " + " ".join(linkflags) + if not LIBS or not LIBPATH: return "" - - linkflags = f" -L{LIBPATH[0]} -Wl,--whole-archive" + + linkflags = [] + for path in LIBPATH: + linkflags.append(quote_spaces(f"-L{os.fspath(path)}")) + linkflags.append("-Wl,--whole-archive") for lib in LIBS: - linkflags += f" -l{lib}" - linkflags += " -Wl,--no-whole-archive -Wl,--allow-multiple-definition" + linkflags.append(f"-l{lib}") + linkflags.extend([ + "-Wl,--no-whole-archive", + "-Wl,--allow-multiple-definition", + ]) - return linkflags + return " " + " ".join(linkflags) def clean_user_apps_build(build_root): @@ -368,25 +527,45 @@ def build_example_usrapp(cwd, has_func, rtconfig, build_root=None): try: # Import build support functions import sys - sys.path.append(os.path.join(cwd, '../rust/tools')) + tools_dir = os.path.abspath(os.path.join(cwd, '..', '..', 'tools')) + if tools_dir not in sys.path: + sys.path.append(tools_dir) import build_support as rust_build_support - + + build_root = normalize_build_root(build_root, cwd) + enabled_apps = [ + app_dir for app_dir in discover_user_apps(cwd) + if should_build_app(app_dir, has_func) + ] + if not enabled_apps: + print('No user applications enabled for Rust build') + return LIBS, LIBPATH, LINKFLAGS + target = rust_build_support.detect_rust_target(has_func, rtconfig) + if not target: + raise UserAppBuildError('Could not detect Rust target for user application build') debug = bool(has_func('RUST_DEBUG_BUILD')) rustflags = rust_build_support.make_rustflags(rtconfig, target) - LIBS, LIBPATH, success_count, total_count = build_all_user_apps( - cwd, target, debug, rustflags, build_root, has_func + if not rust_build_support.ensure_rust_target_installed(target): + raise UserAppBuildError('Rust target is not installed; user application build failed') + + platform = getattr(rtconfig, 'PLATFORM', None) + LIBS, LIBPATH, LIBFILES, UNDEFINED_SYMBOLS, success_count, total_count = build_all_user_apps( + cwd, target, debug, rustflags, build_root, has_func, platform == 'armclang' ) - if success_count == 0 and total_count > 0: - print(f'Warning: Failed to build all {total_count} user applications') - elif success_count > 0: - LINKFLAGS = generate_linkflags(LIBS, LIBPATH) - print(f'Example user apps linked successfully') + if success_count > 0: + LINKFLAGS = generate_linkflags(LIBS, LIBPATH, platform, LIBFILES, UNDEFINED_SYMBOLS) + if platform == 'armclang': + LIBS = [] + LIBPATH = [] + print('Example user apps linked successfully') + else: + print('No user applications enabled for Rust build') - except UserAppBuildError as e: - print(f'Error: {e}') + except UserAppBuildError: + raise except Exception as e: - print(f'Unexpected error during user apps build: {e}') + raise UserAppBuildError(f'Unexpected error during user apps build: {e}') from e - return LIBS, LIBPATH, LINKFLAGS \ No newline at end of file + return LIBS, LIBPATH, LINKFLAGS diff --git a/components/rust/tools/feature_config_component.py b/components/rust/tools/feature_config_component.py index 221d529abe07..e0c7e2f510b9 100644 --- a/components/rust/tools/feature_config_component.py +++ b/components/rust/tools/feature_config_component.py @@ -15,6 +15,7 @@ def setup_all_component_features(): 'RUST_LOG_COMPONENT': { 'feature': 'enable-log', 'dependencies': ['em_component_log'], + 'export_symbols': ['__rust_component_registry_component_seg'], 'description': 'Enable Rust logging component integration' }, }) diff --git a/tools/requirements.txt b/tools/requirements.txt index 33b60c4c741d..bfc09635a3cd 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -2,4 +2,5 @@ scons>=4.0.1 requests>=2.27.1 tqdm>=4.67.1 kconfiglib>=13.7.1 -PyYAML>=6.0 \ No newline at end of file +PyYAML>=6.0 +toml>=0.10.2