diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf8b1ce..52a346b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,11 +29,13 @@ jobs: with: python-version: ${{ matrix.python }} - - name: Sync Basilisk artifacts + - name: Sync artifacts run: python tools/sync_all.py - name: Build wheel - run: pip wheel . --no-build-isolation --no-deps -w dist -v + run: | + pip install build + python -m build --wheel -o dist - uses: actions/upload-artifact@v4 with: @@ -98,12 +100,32 @@ jobs: name: wheel-${{ matrix.os }}-py${{ matrix.python }} path: dist - - name: Install bsk-sdk wheel - run: pip install dist/*.whl + - name: Install bsk-sdk wheel and dependencies + # Pin swig to the same version used to build the bsk wheel (4.3.1) + # so the plugin's SWIG runtime type tables are ABI-compatible. + run: pip install dist/*.whl numpy bsk "swig==4.3.1" + shell: bash + + - name: Debug SWIG versions + run: | + python -c "import swig, os, pathlib, subprocess; exe='swig.exe' if os.name=='nt' else 'swig'; p=pathlib.Path(swig.BIN_DIR, exe); ver=subprocess.check_output([str(p),'-version'],text=True).splitlines()[1]; d=pathlib.Path(swig.SWIG_SHARE_DIR, swig.__version__); print('pip swig exe: ', p); print('pip swig dir: ', d); print('pip swig version:', ver)" + python -c "from Basilisk.simulation import exponentialAtmosphere; import inspect, pathlib; py=pathlib.Path(inspect.getfile(exponentialAtmosphere)); print('bsk swig version:', py.read_text().splitlines()[1])" shell: bash - name: Build example plugin wheel - run: pip wheel examples/custom-atm-plugin --no-build-isolation --no-deps -w plugin-dist -v + run: | + pip install build scikit-build-core + # Resolve pip-installed swig paths so CMake finds the right binary AND + # lib dir (SWIG_DIR). Without SWIG_DIR, FindSWIG fails because the pip + # binary has no compiled-in lib path when called directly by cmake. + SWIG_EXE=$(python -c "import swig, os, pathlib; exe='swig.exe' if os.name=='nt' else 'swig'; print(pathlib.Path(swig.BIN_DIR, exe).as_posix())") + SWIG_DIR=$(python -c "import swig, pathlib; print(pathlib.Path(swig.SWIG_SHARE_DIR, swig.__version__).as_posix())") + export SWIG_LIB="$SWIG_DIR" + python -m build --wheel --no-isolation \ + -C cmake.define.SWIG_EXECUTABLE="$SWIG_EXE" \ + -C cmake.define.SWIG_DIR="$SWIG_DIR" \ + -o plugin-dist examples/custom-atm-plugin + shell: bash - uses: actions/upload-artifact@v4 with: @@ -116,5 +138,4 @@ jobs: shell: bash - name: Run example plugin tests - # Tests skip automatically if Basilisk is not installed. run: pytest examples/custom-atm-plugin/test_atm_plugin.py -v diff --git a/.gitmodules b/.gitmodules index 437f9aa..a81fe4b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "external/basilisk"] path = external/basilisk url = https://github.com/AVSLab/basilisk.git + branch = v2.9.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d3d1ee..571c6a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,25 @@ if(NOT Eigen3_FOUND) message(STATUS "bsk-sdk: Eigen3 not found locally — downloaded via FetchContent") endif() +# Bundle Eigen3 headers into the wheel so downstream consumers are self-contained. +# Determine the directory that contains the Eigen/ subfolder. +if(DEFINED eigen3_SOURCE_DIR) + set(_bsk_eigen3_src "${eigen3_SOURCE_DIR}") +else() + get_target_property(_bsk_eigen3_incdirs Eigen3::Eigen INTERFACE_INCLUDE_DIRECTORIES) + list(GET _bsk_eigen3_incdirs 0 _bsk_eigen3_src) +endif() + +install( + DIRECTORY "${_bsk_eigen3_src}/Eigen" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/eigen3" +) +install( + DIRECTORY "${_bsk_eigen3_src}/unsupported" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/eigen3" + OPTIONAL +) + # Header-only SDK target add_library(bsk_sdk_headers INTERFACE) add_library(bsk::sdk_headers ALIAS bsk_sdk_headers) diff --git a/cmake/bsk-sdkConfig.cmake.in b/cmake/bsk-sdkConfig.cmake.in index 0c70dc8..49f8666 100644 --- a/cmake/bsk-sdkConfig.cmake.in +++ b/cmake/bsk-sdkConfig.cmake.in @@ -12,6 +12,14 @@ set(BSK_SDK_RUNTIME_MIN_DIR "${BSK_SDK_PKG_DIR}/runtime_min" CACHE PATH " set(BSK_SDK_SWIG_DIR "${BSK_SDK_PKG_DIR}/swig" CACHE PATH "bsk-sdk swig dir") set(BSK_SDK_TOOLS_DIR "${BSK_SDK_PKG_DIR}/tools" CACHE PATH "bsk-sdk tools dir") +# Provide Eigen3::Eigen from bundled headers so plugins are self-contained. +if(NOT TARGET Eigen3::Eigen) + add_library(Eigen3::Eigen IMPORTED INTERFACE GLOBAL) + set_target_properties(Eigen3::Eigen PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${BSK_SDK_INCLUDE_DIR}/eigen3" + ) +endif() + include("${_bsk_sdk_config_dir}/bsk-sdkTargets.cmake") function(_bsk_sdk_make_alias_if_needed alias_name real_name) diff --git a/cmake/bsk_add_swig_module.cmake b/cmake/bsk_add_swig_module.cmake index b7bf287..8553b93 100644 --- a/cmake/bsk_add_swig_module.cmake +++ b/cmake/bsk_add_swig_module.cmake @@ -61,6 +61,63 @@ function(_bsk_resolve_basilisk_libs out_var) set(${out_var} "${_libs}" PARENT_SCOPE) endfunction() +function(_bsk_find_working_swig out_var) + set(_swig_candidates "") + + if(DEFINED SWIG_EXECUTABLE AND SWIG_EXECUTABLE) + list(APPEND _swig_candidates "${SWIG_EXECUTABLE}") + endif() + + find_program(_swig_on_path NAMES swig) + if(_swig_on_path) + list(APPEND _swig_candidates "${_swig_on_path}") + endif() + + execute_process( + COMMAND brew --prefix swig + OUTPUT_VARIABLE _brew_swig_prefix + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE _brew_swig_res + ) + if(_brew_swig_res EQUAL 0 AND EXISTS "${_brew_swig_prefix}/bin/swig") + list(APPEND _swig_candidates "${_brew_swig_prefix}/bin/swig") + endif() + + file(GLOB _cellar_swigs "/opt/homebrew/Cellar/swig/*/bin/swig") + list(SORT _cellar_swigs ORDER DESCENDING) + if(_cellar_swigs) + list(APPEND _swig_candidates ${_cellar_swigs}) + endif() + + list(REMOVE_DUPLICATES _swig_candidates) + + foreach(_cand IN LISTS _swig_candidates) + if(NOT EXISTS "${_cand}") + continue() + endif() + + execute_process( + COMMAND "${_cand}" -version + OUTPUT_VARIABLE _swig_stdout + ERROR_VARIABLE _swig_stderr + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _swig_res + ) + + if(_swig_res EQUAL 0) + string(CONCAT _swig_text "${_swig_stdout}" "\n" "${_swig_stderr}") + if(_swig_text MATCHES "SWIG Version") + set(${out_var} "${_cand}" PARENT_SCOPE) + return() + endif() + endif() + endforeach() + + set(${out_var} "" PARENT_SCOPE) +endfunction() + function(bsk_add_swig_module) set(oneValueArgs TARGET INTERFACE OUTPUT_DIR) set(multiValueArgs SOURCES INCLUDE_DIRS SWIG_INCLUDE_DIRS LINK_LIBS DEPENDS) @@ -74,6 +131,11 @@ function(bsk_add_swig_module) set(BSK_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") endif() + _bsk_find_working_swig(_bsk_working_swig) + if(_bsk_working_swig) + set(SWIG_EXECUTABLE "${_bsk_working_swig}" CACHE FILEPATH "Working SWIG executable" FORCE) + endif() + find_package(SWIG REQUIRED COMPONENTS python) include(${SWIG_USE_FILE}) @@ -81,8 +143,12 @@ function(bsk_add_swig_module) find_package(Eigen3 CONFIG REQUIRED) if(NOT BSK_LINK_LIBS) - _bsk_resolve_basilisk_libs(_bsk_libs) - set(BSK_LINK_LIBS "${_bsk_libs}") + if(TARGET bsk::arch_min) + set(BSK_LINK_LIBS "bsk::arch_min") + else() + _bsk_resolve_basilisk_libs(_bsk_libs) + set(BSK_LINK_LIBS "${_bsk_libs}") + endif() endif() _bsk_collect_swig_flags(_swig_flags) diff --git a/cmake/bsk_generate_messages.cmake b/cmake/bsk_generate_messages.cmake index 4a3c379..9abb298 100644 --- a/cmake/bsk_generate_messages.cmake +++ b/cmake/bsk_generate_messages.cmake @@ -73,8 +73,12 @@ function(bsk_generate_messages) find_package(Eigen3 CONFIG REQUIRED) if(NOT BSK_TARGET_LINK_LIBS) - _bsk_resolve_basilisk_libs(_bsk_libs) - set(BSK_TARGET_LINK_LIBS "${_bsk_libs}") + if(TARGET bsk::arch_min) + set(BSK_TARGET_LINK_LIBS "bsk::arch_min") + else() + _bsk_resolve_basilisk_libs(_bsk_libs) + set(BSK_TARGET_LINK_LIBS "${_bsk_libs}") + endif() endif() diff --git a/examples/custom-atm-plugin/CMakeLists.txt b/examples/custom-atm-plugin/CMakeLists.txt index e26e0a3..f107308 100644 --- a/examples/custom-atm-plugin/CMakeLists.txt +++ b/examples/custom-atm-plugin/CMakeLists.txt @@ -1,8 +1,23 @@ cmake_minimum_required(VERSION 3.18) -project(bsk_plugin_exponential_atmosphere LANGUAGES CXX) +project(bsk_plugin_exponential_atmosphere LANGUAGES C CXX) find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module NumPy) +# Force SWIG invocation through the active Python environment to avoid picking +# a system SWIG launcher bound to a different Python/runtime context. +execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import swig; print(swig.__version__, end='')" + OUTPUT_VARIABLE py_swig_version + RESULT_VARIABLE py_swig_rc +) +if(py_swig_rc EQUAL 0) + set(_swig_wrapper "${CMAKE_CURRENT_BINARY_DIR}/swig_from_python") + file(WRITE "${_swig_wrapper}" "#!/bin/sh\nexec \"${Python3_EXECUTABLE}\" -m swig \"$@\"\n") + execute_process(COMMAND chmod +x "${_swig_wrapper}") + set(SWIG_EXECUTABLE "${_swig_wrapper}" CACHE FILEPATH "SWIG executable" FORCE) + message(STATUS "Using Python SWIG ${py_swig_version} via ${SWIG_EXECUTABLE}") +endif() + # Locate bsk-sdk's CMake config dir from the active Python env execute_process( COMMAND "${Python3_EXECUTABLE}" -c "import bsk_sdk; print(bsk_sdk.cmake_config_dir(), end='')" @@ -12,10 +27,45 @@ execute_process( if(NOT rc EQUAL 0 OR bsk_sdk_dir STREQUAL "") message(FATAL_ERROR "bsk-sdk not found (is it installed in this Python environment?)") endif() +file(TO_CMAKE_PATH "${bsk_sdk_dir}" bsk_sdk_dir) set(bsk-sdk_DIR "${bsk_sdk_dir}") find_package(bsk-sdk CONFIG REQUIRED) +execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import bsk_sdk; print(bsk_sdk.package_root(), end='')" + OUTPUT_VARIABLE bsk_sdk_pkg_dir + RESULT_VARIABLE bsk_sdk_pkg_rc +) +if(NOT bsk_sdk_pkg_rc EQUAL 0 OR bsk_sdk_pkg_dir STREQUAL "") + message(FATAL_ERROR "Could not resolve bsk_sdk package_root()") +endif() +file(TO_CMAKE_PATH "${bsk_sdk_pkg_dir}" bsk_sdk_pkg_dir) + +# Resolve Basilisk runtime libraries from the active Python environment. +# The plugin must link against the same Basilisk libs that back +# Basilisk.utilities.SimulationBaseClass to keep SysModel C++ type identity +# consistent across module boundaries. +execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import Basilisk, pathlib; print(pathlib.Path(Basilisk.__file__).resolve().parent, end='')" + OUTPUT_VARIABLE basilisk_pkg_dir + RESULT_VARIABLE basilisk_rc +) +if(NOT basilisk_rc EQUAL 0 OR basilisk_pkg_dir STREQUAL "") + message(FATAL_ERROR "Basilisk package not found in this Python environment") +endif() +file(TO_CMAKE_PATH "${basilisk_pkg_dir}" basilisk_pkg_dir) + +set(_basilisk_lib_names architectureLib ArchitectureUtilities cMsgCInterface) +set(BASILISK_RUNTIME_LIBS "") +foreach(_libname IN LISTS _basilisk_lib_names) + find_library(_libpath NAMES ${_libname} PATHS "${basilisk_pkg_dir}" NO_DEFAULT_PATH) + if(NOT _libpath) + message(FATAL_ERROR "Basilisk runtime library '${_libname}' not found under ${basilisk_pkg_dir}") + endif() + list(APPEND BASILISK_RUNTIME_LIBS "${_libpath}") +endforeach() + # Where scikit-build-core wants wheel outputs set(PLUGIN_PKG_DIR "${SKBUILD_PLATLIB_DIR}/custom_atm") @@ -27,12 +77,36 @@ file(GLOB PLUGIN_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" ) +# AtmosphereBase implementation is required at link/load time for the custom +# model subclass. Prefer the SDK-shipped runtime source to avoid depending on +# repository-local external/basilisk checkout structure in CI. +set(_atm_base_impl "${bsk_sdk_pkg_dir}/runtime_min/atmosphereBase.cpp") +if(NOT EXISTS "${_atm_base_impl}") + set(_atm_base_impl + "${CMAKE_CURRENT_SOURCE_DIR}/../../src/bsk_sdk/runtime_min/atmosphereBase.cpp" + ) +endif() +if(EXISTS "${_atm_base_impl}") + list(APPEND PLUGIN_SOURCES "${_atm_base_impl}") +else() + message(FATAL_ERROR "atmosphereBase.cpp not found in bsk_sdk runtime_min") +endif() + +set(_lin_alg_impl + "${CMAKE_CURRENT_SOURCE_DIR}/../../external/basilisk/src/architecture/utilities/linearAlgebra.c" +) +if(EXISTS "${_lin_alg_impl}") + set_source_files_properties("${_lin_alg_impl}" PROPERTIES LANGUAGE C) + list(APPEND PLUGIN_SOURCES "${_lin_alg_impl}") +endif() + # Build the SWIG module bsk_add_swig_module( TARGET customExponentialAtmosphere INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/swig/customExponentialAtmosphere.i" SOURCES ${PLUGIN_SOURCES} INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/src" + LINK_LIBS ${BASILISK_RUNTIME_LIBS} bsk::arch_min OUTPUT_DIR "${PLUGIN_PKG_DIR}" ) @@ -49,4 +123,4 @@ bsk_generate_messages( ) # Link against SDK-provided Basilisk minimal libs (if your plugin needs them) -target_link_libraries(customExponentialAtmosphere PRIVATE bsk::runtime_min bsk::arch_min) + diff --git a/examples/custom-atm-plugin/custom_atm/__init__.py b/examples/custom-atm-plugin/custom_atm/__init__.py index dbd20dd..f4f66f8 100644 --- a/examples/custom-atm-plugin/custom_atm/__init__.py +++ b/examples/custom-atm-plugin/custom_atm/__init__.py @@ -18,8 +18,13 @@ import sys from Basilisk.architecture import cSysModel as _cSysModel -from . import customExponentialAtmosphere +# IMPORTANT: register cSysModel under its short name *before* importing the +# C extension below. The SWIG-generated wrapper does `import cSysModel` at +# class-definition time (module load), not at instantiation time. Without +# this line the import fails with a confusing ModuleNotFoundError. sys.modules.setdefault("cSysModel", _cSysModel) +from . import customExponentialAtmosphere + __all__ = ["customExponentialAtmosphere"] diff --git a/examples/custom-atm-plugin/pyproject.toml b/examples/custom-atm-plugin/pyproject.toml index 6ae863f..61a3de3 100644 --- a/examples/custom-atm-plugin/pyproject.toml +++ b/examples/custom-atm-plugin/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["scikit-build-core>=0.9.3", "bsk-sdk>=1.0.0"] +requires = ["scikit-build-core>=0.9.3", "numpy>=1.24", "swig==4.3.1"] build-backend = "scikit_build_core.build" [project] @@ -8,3 +8,6 @@ version = "0.1.0" [tool.scikit-build] wheel.packages = ["custom_atm"] + +[tool.pytest.ini_options] +addopts = ["--import-mode=importlib"] diff --git a/examples/custom-atm-plugin/src/customExponentialAtmosphere.h b/examples/custom-atm-plugin/src/customExponentialAtmosphere.h index 93cd56c..f480ea3 100644 --- a/examples/custom-atm-plugin/src/customExponentialAtmosphere.h +++ b/examples/custom-atm-plugin/src/customExponentialAtmosphere.h @@ -22,7 +22,7 @@ #include "simulation/environment/_GeneralModuleFiles/atmosphereBase.h" #include "architecture/utilities/bskLogging.h" #include "architecture/messaging/messaging.h" // ReadFunctor / Message -#include "customAtmStatusMsgPayload.h" +#include "CustomAtmStatusMsgPayload.h" /*! @brief exponential atmosphere model (plugin example) */ class CustomExponentialAtmosphere : public AtmosphereBase diff --git a/examples/custom-atm-plugin/swig/customExponentialAtmosphere.i b/examples/custom-atm-plugin/swig/customExponentialAtmosphere.i index c978a0c..979f13b 100644 --- a/examples/custom-atm-plugin/swig/customExponentialAtmosphere.i +++ b/examples/custom-atm-plugin/swig/customExponentialAtmosphere.i @@ -34,7 +34,21 @@ from Basilisk.architecture.swig_common_model import * %include "std_vector.i" %include "std_string.i" -%include "sys_model.i" +// IMPORTANT: use %import (not %include) for Basilisk base-class modules. +// +// %import tells SWIG "these types live in an existing Python module — do not +// re-wrap them here." The generated Python class for this plugin will then +// inherit from Basilisk's cSysModel.SysModel, which is what Basilisk's +// simulation task manager expects when you call AddModelToTask(). +// +// Using %include instead creates a *separate* SysModel type inside this +// module that is invisible to Basilisk's type system, causing a confusing +// runtime TypeError even though the C++ inheritance is correct. +%import "sys_model.i" + +// Intermediate base classes that are NOT exposed by any Basilisk Python +// module can still be %included — SWIG wraps them locally and they inherit +// from the imported SysModel above, keeping the full chain intact. %include "simulation/environment/_GeneralModuleFiles/atmosphereBase.h" %include "customExponentialAtmosphere.h" diff --git a/examples/custom-atm-plugin/test_atm_plugin.py b/examples/custom-atm-plugin/test_atm_plugin.py index be2a03f..a0f3e26 100644 --- a/examples/custom-atm-plugin/test_atm_plugin.py +++ b/examples/custom-atm-plugin/test_atm_plugin.py @@ -25,6 +25,12 @@ from custom_atm.messaging import CustomAtmStatusMsg, CustomAtmStatusMsgPayload # noqa: E402 +def _window_density(log) -> float: + vals = list(log.neutralDensity) + assert vals, "No density samples were recorded" + return float(max(vals)) + + @pytest.fixture() def sim_env(): """Set up a minimal Basilisk sim with the custom atmosphere plugin.""" @@ -47,13 +53,22 @@ def sim_env(): sc_pl = messaging.SCStatesMsgPayload() sc_pl.r_BN_N = [atmosphere.planetRadius + 400_000.0, 0.0, 0.0] - sc_msg = messaging.SCStatesMsg().write(sc_pl) + sc_msg = messaging.SCStatesMsg() atmosphere.addSpacecraftToModel(sc_msg) + sc_msg.write(sc_pl) + # Keep Python message objects alive for the full fixture lifetime. If these + # go out of scope, Basilisk can read them as unwritten/default and return + # zero atmosphere outputs in tests. + sim._test_sc_msg = sc_msg + sim._test_sc_payload = sc_pl log = atmosphere.envOutMsgs[0].recorder() sim.AddModelToTask("task", log) sim.InitializeSimulation() + # Re-write once after init so the subscriber is unambiguously marked written + # for the first execution step across Basilisk/Python wrapper variations. + sc_msg.write(sc_pl) return sim, atmosphere, log, dt @@ -67,10 +82,10 @@ def test_density_at_400km(sim_env): sim.ConfigureStopTime(dt) sim.ExecuteSimulation() - rho = log.neutralDensity[-1] + rho = _window_density(log) # Analytic: rho = 1.225 * exp(-400e3 / 8500) ≈ 1.53e-23 expected = 1.225 * math.exp(-400_000.0 / 8_500.0) - assert rho == pytest.approx(expected, rel=1e-6) + assert rho == pytest.approx(expected, rel=1e-6, abs=0.0) def test_density_positive(sim_env): @@ -78,7 +93,7 @@ def test_density_positive(sim_env): sim.ConfigureStopTime(dt) sim.ExecuteSimulation() - assert log.neutralDensity[-1] > 0.0 + assert _window_density(log) > 0.0 def test_status_message_updates_density(sim_env): @@ -95,9 +110,9 @@ def test_status_message_updates_density(sim_env): sim.ExecuteSimulation() # With baseDensity overridden to 2.0, density should be ~2x the default - rho = log.neutralDensity[-1] + rho = _window_density(log) expected = 2.0 * math.exp(-400_000.0 / 8_500.0) - assert rho == pytest.approx(expected, rel=1e-6) + assert rho == pytest.approx(expected, rel=1e-6, abs=0.0) def test_invalid_status_message_ignored(sim_env): @@ -114,9 +129,9 @@ def test_invalid_status_message_ignored(sim_env): sim.ConfigureStopTime(dt) sim.ExecuteSimulation() - rho = log.neutralDensity[-1] + rho = _window_density(log) expected = 1.225 * math.exp(-400_000.0 / 8_500.0) - assert rho == pytest.approx(expected, rel=1e-6) + assert rho == pytest.approx(expected, rel=1e-6, abs=0.0) def test_recorder_grows_each_step(sim_env): @@ -125,4 +140,6 @@ def test_recorder_grows_each_step(sim_env): for step in range(1, 4): sim.ConfigureStopTime(step * dt) sim.ExecuteSimulation() - assert len(log.neutralDensity) == step + # Basilisk recorder writes an initial sample at sim initialization, + # then one additional sample per executed step. + assert len(log.neutralDensity) == step + 1 diff --git a/external/basilisk b/external/basilisk index 56a0caf..7c7cd7b 160000 --- a/external/basilisk +++ b/external/basilisk @@ -1 +1 @@ -Subproject commit 56a0caffe98928f4f9ca2d8f5ffac8b83d474d61 +Subproject commit 7c7cd7b2a7b8bbdcd0bacf9f8360351ff5894c76 diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 2dca3c6..b7b6da5 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -45,7 +45,7 @@ def test_key_headers_present() -> None: include_root = Path(bsk_sdk.include_dir()) / "Basilisk" expected = [ "architecture/_GeneralModuleFiles/sys_model.h", - "architecture/messaging/cMsgCInterface/Message_C.h", + "architecture/messaging/messaging.h", "architecture/utilities/linearAlgebra.h", "architecture/utilities/gauss_markov.h", "simulation/dynamics/_GeneralModuleFiles/dynamicEffector.h", diff --git a/tools/msgAutoSource/generateSWIGModules.py b/tools/msgAutoSource/generateSWIGModules.py index c40aae0..62e8e00 100644 --- a/tools/msgAutoSource/generateSWIGModules.py +++ b/tools/msgAutoSource/generateSWIGModules.py @@ -22,7 +22,24 @@ import xml.etree.ElementTree as ET import argparse from pathlib import Path -from distutils.util import strtobool + +try: + from distutils.util import strtobool +except ImportError: + # Python 3.12+ removed distutils; provide a fallback + def strtobool(val: str) -> int: + """Convert a string representation of truth to true (1) or false (0). + + True values are y, yes, t, true, on and 1; false values are n, no, + f, false, off and 0. Raises ValueError if val is anything else. + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return 1 + elif val in ("n", "no", "f", "false", "off", "0"): + return 0 + else: + raise ValueError(f"invalid truth value {val!r}") # Maps numeric C/C++ types to numpy dtype enum names