SEDSnet 4.0.0

A memory safe, no_std-capable networking stack with routing, discovery, reliability, and Rust/C/Python bindings.
Documentation
cmake_minimum_required(VERSION 3.22)
project(sedsnet_cmake NONE)

# --- Find Python interpreter ---
find_package(Python3 COMPONENTS Interpreter REQUIRED)
set(PYTHON_EXECUTABLE "${Python3_EXECUTABLE}")

set(SEDSNET_DIR_DEFAULT "${CMAKE_CURRENT_LIST_DIR}/")
set(SEDSNET_DIR "${SEDSNET_DIR_DEFAULT}" CACHE PATH "Path to sedsnet crate root")
get_filename_component(RUST_DIR "${SEDSNET_DIR}" REALPATH)

# ============================================================
# Config overrides forwarded into the Rust build (via build.py)
# ============================================================

# Device identifier override for Rust (passed to build.py)
set(SEDSNET_DEVICE_IDENTIFIER "" CACHE STRING
    "Override DEVICE_IDENTIFIER passed to build.py (empty = use default)")

# New: proc-macro env var (define_stack_payload!(env = \"MAX_STACK_PAYLOAD\", default = ...))
set(SEDSNET_MAX_STACK_PAYLOAD "" CACHE STRING
    "Override MAX_STACK_PAYLOAD used by define_stack_payload! (empty = use macro default)")

set(SEDSNET_MAX_QUEUE_BUDGET "" CACHE STRING
    "Override MAX_QUEUE_BUDGET shared queue budget in bytes (empty = use Rust default)")

set(SEDSNET_MAX_QUEUE_SIZE "" CACHE STRING
    "Legacy alias for SEDSNET_MAX_QUEUE_BUDGET (empty = ignored)")

set(SEDSNET_MAX_RECENT_RX_IDS "" CACHE STRING
    "Override MAX_RECENT_RX_IDS preallocated recent packet ID entries (empty = use Rust default)")

# Schema JSON path override (forwarded as SEDSNET_SCHEMA_PATH)
set(SEDSNET_SCHEMA_PATH "" CACHE STRING
    "Override telemetry_config.json path (empty = use default)")

# For each env var you want to set, define a CMake var named:
#   SEDSNET_ENV_<ENVKEY>
# Example:
#   set(SEDSNET_ENV_QUEUE_GROW_STEP "2.0" CACHE STRING "" FORCE)
# Prefer the dedicated cache variables above for MAX_QUEUE_BUDGET and MAX_RECENT_RX_IDS.
#
# These will be forwarded as build.py args: env:ENVKEY=VALUE

# ============================================================
# Detect embedded build
# ============================================================
option(SEDSNET_EMBEDDED_BUILD "Build sedsnet for embedded" OFF)
option(SEDSNET_PREFER_DYNAMIC "Prefer dynamic linking for host builds when supported" ON)
option(SEDSNET_FORCE_RELEASE "Build sedsnet in release profile even when the parent CMake build is Debug" OFF)
option(SEDSNET_ENABLE_C_WRAPPER
    "Build the optional native C wrapper target sedsnet::c_wrapper"
    OFF)
option(SEDSNET_ENABLE_CPP_WRAPPER
    "Expose the optional header-only C++ convenience target sedsnet::cpp_wrapper"
    OFF)
option(SEDSNET_ENABLE_CRYPTOGRAPHY
    "Enable cryptography provider APIs and define SEDS_ENABLE_CRYPTOGRAPHY for C/C++ users"
    ON)

# Auto-detect embedded if user hasn't forced it
if (NOT SEDSNET_EMBEDDED_BUILD)
    if (CMAKE_C_COMPILER MATCHES "arm-none-eabi"
            OR CMAKE_TOOLCHAIN_FILE MATCHES "stm32"
            OR CMAKE_TOOLCHAIN_FILE MATCHES "gcc-arm-none-eabi"
            OR DEFINED ENV{STM32CUBEIDE_DIR}
    )
        set(SEDSNET_EMBEDDED_BUILD ON CACHE BOOL "Build sedsnet for embedded" FORCE)
    endif ()
endif ()

# ============================================================
# Rust target triple selection
# ============================================================
if (NOT DEFINED SEDSNET_TARGET)
    if (SEDSNET_EMBEDDED_BUILD)
        set(SEDSNET_TARGET "thumbv7em-none-eabihf" CACHE STRING "Rust target triple for sedsnet (empty = host default)")
    else ()
        set(SEDSNET_TARGET "" CACHE STRING "Rust target triple for sedsnet (empty = host default)")
    endif ()
endif ()

set(RUST_TRIPLE "${SEDSNET_TARGET}")

# ============================================================
# Compute Rust output paths
# ============================================================
set(RELEASE_PROFILE_DIR "release")
if (SEDSNET_EMBEDDED_BUILD)
    set(RELEASE_PROFILE_DIR "release-embedded")
endif ()

if (SEDSNET_FORCE_RELEASE)
    set(SEDSNET_ACTIVE_PROFILE "${RELEASE_PROFILE_DIR}")
else ()
    set(SEDSNET_ACTIVE_PROFILE "$<IF:$<CONFIG:Debug>,debug,${RELEASE_PROFILE_DIR}>")
endif ()

if (RUST_TRIPLE STREQUAL "")
    set(RS_STATIC_LIB_DBG "${RUST_DIR}/target/debug/libsedsnet.a")
    set(RS_STATIC_LIB_REL "${RUST_DIR}/target/release/libsedsnet.a")
else ()
    set(RS_STATIC_LIB_DBG "${RUST_DIR}/target/${RUST_TRIPLE}/debug/libsedsnet.a")
    set(RS_STATIC_LIB_REL "${RUST_DIR}/target/${RUST_TRIPLE}/${RELEASE_PROFILE_DIR}/libsedsnet.a")
endif ()

set(RS_LIB_DBG "${RS_STATIC_LIB_DBG}")
set(RS_LIB_REL "${RS_STATIC_LIB_REL}")
if (SEDSNET_FORCE_RELEASE)
    set(RS_LIB_DBG "${RS_LIB_REL}")
endif ()
set(RS_IMPORTED_KIND STATIC)
set(SEDSNET_LINK_DYNAMIC OFF)
set(RS_SHARED_LIB_DBG "")
set(RS_SHARED_LIB_REL "")

set(RS_INCLUDE "${RUST_DIR}/C-Headers")
set(RS_HEADER "${RS_INCLUDE}/sedsnet.h")
if (NOT EXISTS "${RS_HEADER}")
    message(FATAL_ERROR "Static sedsnet C header is missing: ${RS_HEADER}")
endif ()

# ============================================================
# Build.py args (for logging only)
# ============================================================
set(RS_ARGS_STR "")
if (SEDSNET_EMBEDDED_BUILD)
    set(RS_ARGS_STR "${RS_ARGS_STR} embedded")
endif ()
if (NOT RUST_TRIPLE STREQUAL "")
    set(RS_ARGS_STR "${RS_ARGS_STR} target=${RUST_TRIPLE}")
endif ()
if (NOT SEDSNET_DEVICE_IDENTIFIER STREQUAL "")
    set(RS_ARGS_STR "${RS_ARGS_STR} device_id=${SEDSNET_DEVICE_IDENTIFIER}")
endif ()
if (NOT SEDSNET_MAX_STACK_PAYLOAD STREQUAL "")
    set(RS_ARGS_STR "${RS_ARGS_STR} max_stack_payload=${SEDSNET_MAX_STACK_PAYLOAD}")
endif ()
set(_SEDSNET_EFFECTIVE_MAX_QUEUE_BUDGET "${SEDSNET_MAX_QUEUE_BUDGET}")
if (_SEDSNET_EFFECTIVE_MAX_QUEUE_BUDGET STREQUAL "" AND NOT SEDSNET_MAX_QUEUE_SIZE STREQUAL "")
    set(_SEDSNET_EFFECTIVE_MAX_QUEUE_BUDGET "${SEDSNET_MAX_QUEUE_SIZE}")
endif ()
if (NOT _SEDSNET_EFFECTIVE_MAX_QUEUE_BUDGET STREQUAL "")
    set(RS_ARGS_STR "${RS_ARGS_STR} max_queue_budget=${_SEDSNET_EFFECTIVE_MAX_QUEUE_BUDGET}")
endif ()
if (NOT SEDSNET_MAX_RECENT_RX_IDS STREQUAL "")
    set(RS_ARGS_STR "${RS_ARGS_STR} max_recent_rx_ids=${SEDSNET_MAX_RECENT_RX_IDS}")
endif ()
if (NOT SEDSNET_SCHEMA_PATH STREQUAL "")
    set(RS_ARGS_STR "${RS_ARGS_STR} schema_path=${SEDSNET_SCHEMA_PATH}")
endif ()
if (SEDSNET_ENABLE_CRYPTOGRAPHY)
    set(RS_ARGS_STR "${RS_ARGS_STR} cryptography")
endif ()

if (SEDSNET_FORCE_RELEASE)
    set(RS_ARGS_STR "${RS_ARGS_STR} release")
else ()
    # Show release in log string
    set(RS_ARG_REL " $<$<CONFIG:Release>:release>")
    set(RS_ARGS_STR "${RS_ARGS_STR} ${RS_ARG_REL}")
endif ()

# ============================================================
# Configure-time args passed to build.py (list form)
# ============================================================
set(RS_ARGS_BOOTSTRAP "")

if (SEDSNET_EMBEDDED_BUILD)
    list(APPEND RS_ARGS_BOOTSTRAP "embedded")
endif ()

if (NOT RUST_TRIPLE STREQUAL "")
    list(APPEND RS_ARGS_BOOTSTRAP "target=${RUST_TRIPLE}")
endif ()

# NOTE: When using multi-config generators (VS/Xcode), CMAKE_BUILD_TYPE is empty.
# We rely on the build step to rebuild as needed anyway; default to Debug.
if (SEDSNET_FORCE_RELEASE OR CMAKE_BUILD_TYPE STREQUAL "Release")
    list(APPEND RS_ARGS_BOOTSTRAP "release")
endif ()

if (NOT SEDSNET_DEVICE_IDENTIFIER STREQUAL "")
    list(APPEND RS_ARGS_BOOTSTRAP "device_id=${SEDSNET_DEVICE_IDENTIFIER}")
endif ()

# New: forward proc-macro env var
if (NOT SEDSNET_MAX_STACK_PAYLOAD STREQUAL "")
    list(APPEND RS_ARGS_BOOTSTRAP "max_stack_payload=${SEDSNET_MAX_STACK_PAYLOAD}")
endif ()
if (NOT _SEDSNET_EFFECTIVE_MAX_QUEUE_BUDGET STREQUAL "")
    list(APPEND RS_ARGS_BOOTSTRAP "max_queue_budget=${_SEDSNET_EFFECTIVE_MAX_QUEUE_BUDGET}")
endif ()
if (NOT SEDSNET_MAX_RECENT_RX_IDS STREQUAL "")
    list(APPEND RS_ARGS_BOOTSTRAP "max_recent_rx_ids=${SEDSNET_MAX_RECENT_RX_IDS}")
endif ()
if (NOT SEDSNET_SCHEMA_PATH STREQUAL "")
    list(APPEND RS_ARGS_BOOTSTRAP "schema_path=${SEDSNET_SCHEMA_PATH}")
endif ()
if (SEDSNET_ENABLE_CRYPTOGRAPHY)
    list(APPEND RS_ARGS_BOOTSTRAP "cryptography")
endif ()

# New: forward arbitrary option_env config vars: SEDSNET_ENV_<KEY> => env:KEY=VALUE
get_cmake_property(_all_cache_vars CACHE_VARIABLES)
foreach (_v ${_all_cache_vars})
    if (_v MATCHES "^SEDSNET_ENV_(.+)$")
        string(REGEX REPLACE "^SEDSNET_ENV_" "" _env_key "${_v}")
        # Value comes from the cache variable
        set(_env_val "${${_v}}")
        if (NOT _env_val STREQUAL "")
            list(APPEND RS_ARGS_BOOTSTRAP "env:${_env_key}=${_env_val}")
            # Also append to logging string
            set(RS_ARGS_STR "${RS_ARGS_STR} env:${_env_key}=${_env_val}")
        endif ()
    endif ()
endforeach ()

# Keep Rust staticlib object deployment target aligned with C/C++ link target on macOS.
set(_RS_ENV_CMD ${CMAKE_COMMAND} -E env)
if (APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET)
    list(APPEND _RS_ENV_CMD "MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif ()

# ============================================================
# Track Rust source inputs
# ============================================================
file(GLOB_RECURSE RS_SRC CONFIGURE_DEPENDS
    "${RUST_DIR}/src/*.rs"
    "${RUST_DIR}/build.rs"
    "${RUST_DIR}/Cargo.toml"
    "${RUST_DIR}/Cargo.lock"
    "${RS_HEADER}"
    "${RUST_DIR}/c-wrapper/*.c"
    "${RUST_DIR}/c-wrapper/*.h"
    "${RUST_DIR}/c-wrapper/*.hpp"
)

if (SEDSNET_FORCE_RELEASE)
    set(RS_BUILD_OUTPUTS "${RS_LIB_REL}")
else ()
    set(RS_BUILD_OUTPUTS "${RS_LIB_DBG}" "${RS_LIB_REL}")
endif ()

# ============================================================
# Main build rule
# ============================================================
add_custom_command(
    OUTPUT ${RS_BUILD_OUTPUTS}
    COMMAND ${_RS_ENV_CMD} "${PYTHON_EXECUTABLE}" build.py ${RS_ARGS_BOOTSTRAP}
    WORKING_DIRECTORY "${RUST_DIR}"
    DEPENDS ${RS_SRC}
    COMMENT "Building sedsnet (${SEDSNET_ACTIVE_PROFILE}) ${RS_ARGS_STR}"
    USES_TERMINAL
    VERBATIM
)

add_custom_target(sedsnet_build
    DEPENDS ${RS_BUILD_OUTPUTS}
)

# ============================================================
# Import library for C/C++
# ============================================================
add_library(sedsnet ${RS_IMPORTED_KIND} IMPORTED GLOBAL)
set_target_properties(sedsnet PROPERTIES
    IMPORTED_LOCATION "${RS_LIB_DBG}"
    IMPORTED_LOCATION_DEBUG "${RS_LIB_DBG}"
    IMPORTED_LOCATION_RELEASE "${RS_LIB_REL}"
    INTERFACE_INCLUDE_DIRECTORIES "${RS_INCLUDE}"
)
if (SEDSNET_ENABLE_CRYPTOGRAPHY)
    target_compile_definitions(sedsnet INTERFACE SEDS_ENABLE_CRYPTOGRAPHY=1)
endif ()

add_dependencies(sedsnet sedsnet_build)

# Security flags for embedded
include(CheckLinkerFlag)
include(CheckCCompilerFlag)
check_linker_flag(C "-Wl,-z,noexecstack" HAVE_LD_NOEXECSTACK)
check_c_compiler_flag("-Wa,--noexecstack" HAVE_AS_NOEXECSTACK)

if (SEDSNET_EMBEDDED_BUILD)
    target_compile_options(sedsnet INTERFACE -Wa,--noexecstack)
    target_link_options(sedsnet INTERFACE -Wl,-z,noexecstack)
endif ()

# Optimize
add_compile_options(-ffunction-sections -fdata-sections -Os)
add_link_options(-Wl,--gc-sections -Wl,--icf=all)

add_library(sedsnet::sedsnet ALIAS sedsnet)

if (SEDSNET_ENABLE_C_WRAPPER)
    add_library(sedsnet_c_wrapper STATIC
        "${RUST_DIR}/c-wrapper/sedsnet_c_wrapper.c"
    )
    target_include_directories(sedsnet_c_wrapper PUBLIC
        "${RUST_DIR}/c-wrapper"
    )
    target_link_libraries(sedsnet_c_wrapper PUBLIC sedsnet)
    if (SEDSNET_ENABLE_CRYPTOGRAPHY)
        target_compile_definitions(sedsnet_c_wrapper PUBLIC SEDS_ENABLE_CRYPTOGRAPHY=1)
    endif ()
    add_dependencies(sedsnet_c_wrapper sedsnet_build)
    add_library(sedsnet::c_wrapper ALIAS sedsnet_c_wrapper)
endif ()

if (SEDSNET_ENABLE_CPP_WRAPPER)
    add_library(sedsnet_cpp_wrapper INTERFACE)
    target_include_directories(sedsnet_cpp_wrapper INTERFACE
        "${RUST_DIR}/c-wrapper"
    )
    target_link_libraries(sedsnet_cpp_wrapper INTERFACE sedsnet)
    if (SEDSNET_ENABLE_CRYPTOGRAPHY)
        target_compile_definitions(sedsnet_cpp_wrapper INTERFACE SEDS_ENABLE_CRYPTOGRAPHY=1)
    endif ()
    add_dependencies(sedsnet_cpp_wrapper sedsnet_build)
    add_library(sedsnet::cpp_wrapper ALIAS sedsnet_cpp_wrapper)
endif ()

set(SEDSNET_LIB_DEBUG "${RS_LIB_DBG}" PARENT_SCOPE)
set(SEDSNET_LIB_RELEASE "${RS_LIB_REL}" PARENT_SCOPE)
set(SEDSNET_STATIC_LIB_DEBUG "${RS_STATIC_LIB_DBG}" PARENT_SCOPE)
set(SEDSNET_STATIC_LIB_RELEASE "${RS_STATIC_LIB_REL}" PARENT_SCOPE)
set(SEDSNET_SHARED_LIB_DEBUG "${RS_SHARED_LIB_DBG}" PARENT_SCOPE)
set(SEDSNET_SHARED_LIB_RELEASE "${RS_SHARED_LIB_REL}" PARENT_SCOPE)
set(SEDSNET_INCLUDE_DIR "${RS_INCLUDE}" PARENT_SCOPE)
set(SEDSNET_C_WRAPPER_INCLUDE_DIR "${RUST_DIR}/c-wrapper" PARENT_SCOPE)

message(STATUS "sedsnet include dir: ${RS_INCLUDE}")
message(STATUS "sedsnet debug lib:  ${RS_LIB_DBG}")
message(STATUS "sedsnet release lib:${RS_LIB_REL}")
if (SEDSNET_LINK_DYNAMIC)
    set(_SEDSNET_LINK_MODE "dynamic")
else ()
    set(_SEDSNET_LINK_MODE "static")
endif ()
message(STATUS "sedsnet link mode: ${_SEDSNET_LINK_MODE}")
message(STATUS "sedsnet C wrapper target: ${SEDSNET_ENABLE_C_WRAPPER}")
message(STATUS "sedsnet C++ wrapper target: ${SEDSNET_ENABLE_CPP_WRAPPER}")
message(STATUS "sedsnet cryptography provider: ${SEDSNET_ENABLE_CRYPTOGRAPHY}")
message(STATUS "sedsnet build args: ${RS_ARGS_STR}")