diff --git a/vinca/pinning.py b/vinca/pinning.py index 19313f4..f42effd 100644 --- a/vinca/pinning.py +++ b/vinca/pinning.py @@ -219,11 +219,69 @@ def _migration_name(name: str) -> str: return name +def _existing_eol_comment_text(source: Any, index: int) -> Optional[str]: + """Return the plain text of a CommentedSeq item's trailing EOL comment, if any.""" + ca = getattr(source, "ca", None) + if ca is None: + return None + entry = ca.items.get(index) + if not entry: + return None + token = entry[0] + if token is None: + return None + return str(token.value).lstrip("#").strip() + + +def _flatten_v1_selectors(value: Any) -> Any: + """Convert v1-style `- if: COND then: [...]` list entries into the legacy + `- VALUE # [COND]` comment-annotated form that rattler-build's variant + config loader actually evaluates lazily per target_platform (unlike the + v1 if/then/else mapping form, which it treats as an opaque literal value + rather than a selector -- confirmed via `Could not parse version spec + for variant key ...: invalid channel` / `multiple bracket sections not + allowed` errors when left unconverted). + + Passthrough items (plain scalars, possibly already carrying their own + `# [selector]` EOL comment) must have that existing comment re-attached + at their new index -- ruamel stores comments keyed by list position on + the *source* CommentedSeq, not on the item itself, so a naive + `result.append(item)` into a freshly created CommentedSeq silently + drops it, turning a platform-scoped entry into an unconditional one. + """ + if not isinstance(value, list): + return value + import ruamel.yaml.comments as _rc + + result = _rc.CommentedSeq() + for old_index, item in enumerate(value): + if isinstance(item, Mapping) and "if" in item and "then" in item: + cond = str(item["if"]) + for entry in item["then"]: + idx = len(result) + result.append(entry) + result.yaml_add_eol_comment(f"[{cond}]", idx) + else_branch = item.get("else") + if else_branch is not None: + not_cond = f"not ({cond})" + for entry in else_branch: + idx = len(result) + result.append(entry) + result.yaml_add_eol_comment(f"[{not_cond}]", idx) + else: + idx = len(result) + result.append(item) + comment_text = _existing_eol_comment_text(value, old_index) + if comment_text: + result.yaml_add_eol_comment(comment_text, idx) + return result + + def _overlay(target: Any, source: Any) -> None: for key, value in source.items(): if key == "migrator_ts" or str(key).startswith("__"): continue - target[key] = value + target[key] = _flatten_v1_selectors(value) def _migration_timestamp(payload: bytes) -> float: diff --git a/vinca/templates/build_ament_cmake.sh.in b/vinca/templates/build_ament_cmake.sh.in index cc25a25..06fb73d 100644 --- a/vinca/templates/build_ament_cmake.sh.in +++ b/vinca/templates/build_ament_cmake.sh.in @@ -69,20 +69,52 @@ if [[ $target_platform =~ emscripten.* ]]; then echo "set(CMAKE_STRIP FALSE) # used by default in pybind11 on .so modules">> $SRC_DIR/__vinca_shared_lib_patch.cmake echo "set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) # fixes an error where numpy header files are not found correctly">> $SRC_DIR/__vinca_shared_lib_patch.cmake - # if [ "${PKG_NAME}" == "ros-humble-examples-rclcpp-minimal-publisher" ] || [ "${PKG_NAME}" == "ros-humble-examples-rclcpp-minimal-subscriber" ] || [ "${PKG_NAME}" == "ros-humble-rclcpp-components" ]; then - # echo "set(CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS \"-s ASSERTIONS=1 -s SIDE_MODULE=1 -sWASM_BIGINT -s USE_PTHREADS=0 -s DEMANGLE_SUPPORT=1 -s ALLOW_MEMORY_GROWTH=1 \")">> $SRC_DIR/__vinca_shared_lib_patch.cmake - # echo "set(CMAKE_SHARED_LIBRARY_CREATE_CXX_FLAGS \"-s ASSERTIONS=1 -s SIDE_MODULE=1 -sWASM_BIGINT -s USE_PTHREADS=0 -s DEMANGLE_SUPPORT=1 -s ALLOW_MEMORY_GROWTH=1 -sASYNCIFY -O3 -s ASYNCIFY_STACK_SIZE=24576 \")">> $SRC_DIR/__vinca_shared_lib_patch.cmake - # echo "set(CMAKE_EXE_LINKER_FLAGS \"-sMAIN_MODULE=1 -sASSERTIONS=1 -fexceptions -lembind -sWASM_BIGINT -s USE_PTHREADS=0 -s DEMANGLE_SUPPORT=1 -sALLOW_MEMORY_GROWTH=1 -sASYNCIFY -O3 -s ASYNCIFY_STACK_SIZE=24576 -L$SRC_DIR/build -L$PREFIX/lib\") # remove SIDE_MODULE from exe linker flags">> $SRC_DIR/__vinca_shared_lib_patch.cmake - # else - echo "set(CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS \"-s ASSERTIONS=1 -s SIDE_MODULE=1 -sWASM_BIGINT -s USE_PTHREADS=0 -s ALLOW_MEMORY_GROWTH=1 -s DEMANGLE_SUPPORT=1 \")">> $SRC_DIR/__vinca_shared_lib_patch.cmake - echo "set(CMAKE_SHARED_LIBRARY_CREATE_CXX_FLAGS \"-s ASSERTIONS=1 -s SIDE_MODULE=1 -sWASM_BIGINT -s USE_PTHREADS=0 -s ALLOW_MEMORY_GROWTH=1 -s DEMANGLE_SUPPORT=1 \")">> $SRC_DIR/__vinca_shared_lib_patch.cmake - echo "set(CMAKE_EXE_LINKER_FLAGS \"-sMAIN_MODULE=1 -sASSERTIONS=1 -fexceptions -lembind -sWASM_BIGINT -s USE_PTHREADS=0 -sALLOW_MEMORY_GROWTH=1 -s DEMANGLE_SUPPORT=1 -L$SRC_DIR/build -L$PREFIX/lib\") # remove SIDE_MODULE from exe linker flags">> $SRC_DIR/__vinca_shared_lib_patch.cmake - # fi + # Real pthreads (USE_PTHREADS=1) are required so that blocking waits + # (std::condition_variable / rmw wait sets) actually work: without a real + # OS-level thread, libc++'s condition_variable timed-wait never wakes up + # on its own, and code that spins/blocks the main thread (e.g. rclcpp's + # executor) hangs forever with no way for the browser's JS event loop + # (and thus wall-clock time) to ever advance underneath it. This must be + # consistent across every emscripten-wasm32 package: mixing a + # pthread-enabled module with a non-pthread one is a hard ABI-level + # mismatch ("memory import shared state mismatch") since a wasm module's + # shared-vs-non-shared linear memory is fixed at compile+link time. + # + # This flag has to be set at COMPILE time too (not just link time) for + # every translation unit -- it bakes in the wasm 'atomics'/'bulk-memory' + # features that the linker later requires when producing shared memory + # ("wasm-ld: error: --shared-memory is disallowed ... because it was not + # compiled with 'atomics' or 'bulk-memory' features"). add_compile_options + # here (via CMAKE_PROJECT_INCLUDE, included right after every project() + # call) applies it to every target compiled in every package. + echo "add_compile_options(\"SHELL: -s USE_PTHREADS=1\")">> $SRC_DIR/__vinca_shared_lib_patch.cmake + + echo "set(CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS \"-s ASSERTIONS=1 -s SIDE_MODULE=1 -sWASM_BIGINT -s USE_PTHREADS=1 -s ALLOW_MEMORY_GROWTH=1 \")">> $SRC_DIR/__vinca_shared_lib_patch.cmake + echo "set(CMAKE_SHARED_LIBRARY_CREATE_CXX_FLAGS \"-s ASSERTIONS=1 -s SIDE_MODULE=1 -sWASM_BIGINT -s USE_PTHREADS=1 -s ALLOW_MEMORY_GROWTH=1 \")">> $SRC_DIR/__vinca_shared_lib_patch.cmake + # CMake's MODULE library type (add_library(... MODULE), what + # pybind11_add_module() uses for Python C extensions e.g. rclpy's + # _rclpy_pybind11) is a distinct target type from SHARED and reads its + # own CMAKE_SHARED_MODULE_CREATE_*_FLAGS variables -- setting only the + # SHARED ones above left every MODULE-type .so linked without + # USE_PTHREADS=1, producing a non-shared-memory module that fails to + # load ("mismatch in shared state of memory") next to the rest of a + # pthreads build, even though its own object files were compiled with + # atomics support and looked fine individually. + echo "set(CMAKE_SHARED_MODULE_CREATE_C_FLAGS \"-s ASSERTIONS=1 -s SIDE_MODULE=1 -sWASM_BIGINT -s USE_PTHREADS=1 -s ALLOW_MEMORY_GROWTH=1 \")">> $SRC_DIR/__vinca_shared_lib_patch.cmake + echo "set(CMAKE_SHARED_MODULE_CREATE_CXX_FLAGS \"-s ASSERTIONS=1 -s SIDE_MODULE=1 -sWASM_BIGINT -s USE_PTHREADS=1 -s ALLOW_MEMORY_GROWTH=1 \")">> $SRC_DIR/__vinca_shared_lib_patch.cmake + # PTHREAD_POOL_SIZE and MAXIMUM_MEMORY are only meaningful on the final + # MAIN_MODULE executable link (they configure the Worker pool and the + # shared SharedArrayBuffer's reserved size respectively -- side modules + # don't have memory of their own, they use the main module's). Emscripten + # requires MAXIMUM_MEMORY to be set explicitly whenever + # ALLOW_MEMORY_GROWTH is combined with USE_PTHREADS, since a shared + # wasm memory's maximum size can't be left unbounded. + echo "set(CMAKE_EXE_LINKER_FLAGS \"-sMAIN_MODULE=1 -sASSERTIONS=1 -fexceptions -lembind -sWASM_BIGINT -s USE_PTHREADS=1 -s PTHREAD_POOL_SIZE=4 -sALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=1024MB -L$SRC_DIR/build -L$PREFIX/lib\") # remove SIDE_MODULE from exe linker flags">> $SRC_DIR/__vinca_shared_lib_patch.cmake export BUILD_TYPE="Debug" export EXTRA_CMAKE_ARGS=" \ -DPYTHON_SOABI="cpython-${ROS_PYTHON_VERSION//./}-wasm32-emscripten" \ - -DRMW_IMPLEMENTATION=rmw_wasm_cpp \ + -DRMW_IMPLEMENTATION=${VINCA_EMSCRIPTEN_RMW_IMPLEMENTATION:-rmw_wasm_cpp} \ -DCMAKE_FIND_ROOT_PATH=$PREFIX \ -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE \ -DCMAKE_PROJECT_INCLUDE=$SRC_DIR/__vinca_shared_lib_patch.cmake \ @@ -92,8 +124,8 @@ if [[ $target_platform =~ emscripten.* ]]; then export CMAKE_GEN="emcmake cmake" export CMAKE_BLD="cmake" - export STATIC_ROSIDL_TYPESUPPORT_C=rosidl_typesupport_introspection_c - export STATIC_ROSIDL_TYPESUPPORT_CPP=rosidl_typesupport_introspection_cpp + export STATIC_ROSIDL_TYPESUPPORT_C=${VINCA_EMSCRIPTEN_STATIC_TYPESUPPORT_C:-rosidl_typesupport_introspection_c} + export STATIC_ROSIDL_TYPESUPPORT_CPP=${VINCA_EMSCRIPTEN_STATIC_TYPESUPPORT_CPP:-rosidl_typesupport_introspection_cpp} else export BUILD_TYPE="Release" export CMAKE_GEN="cmake"