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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/testpr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,13 @@ jobs:
run: |
set +e
if [ "${{ matrix.platform }}" = "emscripten-wasm32" ]; then
pixi run rattler-build build --recipe-dir recipes --target-platform ${{ matrix.platform }} -m ./conda_build_config.yaml -c https://repo.mamba.pm/emscripten-forge/ -c conda-forge -c robostack-staging --skip-existing --test skip 2>&1 | tee rattler-build.log
# https://repo.mamba.pm/emscripten-forge is stale/legacy (empty for
# osx-arm64, ~1/3 the package count for the emscripten-wasm32 target
# vs the channel below, as of 2026-09-07) -- emscripten-forge/recipes'
# own pixi.toml publishes the emscripten SDK + cross-python toolchain
# (needed for host/build deps like emscripten_emscripten-wasm32 and
# cross-python_emscripten-wasm32) to this channel instead.
pixi run rattler-build build --recipe-dir recipes --target-platform ${{ matrix.platform }} -m ./conda_build_config.yaml -c https://repo.prefix.dev/emscripten-forge-4x -c microsoft -c conda-forge -c robostack-staging --skip-existing --test skip 2>&1 | tee rattler-build.log
else
# Without this, resolvo's strict channel priority can refuse a
# just-built local package outright once any other-channel build of
Expand Down
135 changes: 83 additions & 52 deletions conda_build_config.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# Generated by vinca-pinning-render from vinca_pinning.yaml.
# Do not edit this file directly.
c_compiler:
- gcc # [linux]
- clang # [osx]
- vs2022 # [win]
# Please remember to update gcc_compiler_version & clang_compiler_version too.
- gcc # [linux]
- clang # [osx]
- vs2022 # [win]
- emscripten # [emscripten]
c_compiler_version: # [unix]
- 15 # [linux]
- 21 # [osx]
- 14 # [linux and (x86_64 or aarch64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- 15 # [linux]
- 21 # [osx]
- 14 # [linux and (x86_64 or aarch64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
# Matches emscripten-forge/recipes' variant.yaml c_compiler_version; refresh
# alongside cxx_compiler_version and emscripten_emscripten-wasm32 (below) when
# emscripten-forge bumps their emscripten SDK version.
- 4.0.9 # [emscripten]
c_stdlib:
- sysroot # [linux]
- macosx_deployment_target # [osx]
Expand All @@ -18,29 +22,30 @@ m2w64_c_stdlib: # [win]
m2w64_c_stdlib_version: # [win]
- 12 # [win]
c_stdlib_version: # [unix]
- 2.28 # [linux and not riscv64]
- 2.39 # [linux and riscv64]
- 2.28 # [linux and (x86_64 or aarch64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- 14.0 # [osx]
- 2.28 # [linux and not riscv64]
- 2.39 # [linux and riscv64]
- 2.28 # [linux and (x86_64 or aarch64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- 14.0 # [osx]
cxx_compiler:
- gxx # [linux]
- clangxx # [osx]
- vs2022 # [win]
# Please remember to update gxx_compiler_version & clangxx_compiler_version too.
- gxx # [linux]
- clangxx # [osx]
- vs2022 # [win]
- emscripten # [emscripten]
cxx_compiler_version: # [unix]
- 15 # [linux]
- 21 # [osx]
- 14 # [linux and (x86_64 or aarch64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- 15 # [linux]
- 21 # [osx]
- 14 # [linux and (x86_64 or aarch64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- 4.0.9 # [emscripten]
llvm_openmp: # [osx]
- 21 # [osx]
fortran_compiler: # [unix or win]
- gfortran # [unix]
- flang # [win]
fortran_compiler_version: # [unix or win]
- 15 # [unix]
- 5 # [win64]
- 22 # [win and arm64]
- 14 # [linux and (x86_64 or aarch64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- 15 # [linux or osx]
- 5 # [win64]
- 22 # [win and arm64]
- 14 # [linux and (x86_64 or aarch64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
m2w64_c_compiler: # [win]
- gcc # [win]
m2w64_c_compiler_version: # [win]
Expand Down Expand Up @@ -80,7 +85,7 @@ cuda_compiler:
- cuda-nvcc
cuda_compiler_version:
- None
- 12.9 # [((linux and (x86_64 or aarch64)) or win64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- 12.9 # [((linux and (x86_64 or aarch64)) or win64) and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
cuda_compiler_version_min:
- None # [not ((linux and (x86_64 or aarch64)) or win64)]
- 12.9 # [((linux and (x86_64 or aarch64)) or win64)]
Expand Down Expand Up @@ -169,23 +174,28 @@ docker_image: # [os.environ.get("BUILD_PLA
- quay.io/condaforge/linux-anvil-ppc64le:alma10 # [os.environ.get("BUILD_PLATFORM") == "linux-ppc64le" and os.environ.get("DEFAULT_LINUX_VERSION", "alma10") == "alma10"]

zip_keys:
# [unix]
- - c_compiler_version # [unix]
- cxx_compiler_version # [unix]
- fortran_compiler_version # [unix]
# CUDA 13.x requires newer glibc than our current baseline
- c_stdlib_version # [linux and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- cuda_compiler_version # [linux and os.environ.get("CF_CUDA_ENABLED", "False") == "True"]
- - c_compiler_version
- cxx_compiler_version
- - c_stdlib_version # [linux or osx]
- fortran_compiler_version # [linux or osx]
- cuda_compiler_version # [linux or osx]
- - python
- is_python_min
- - libarrow
- libarrow_all
- - root_base
- root_cxx_standard

# armv7l specifics because conda-build sets many things to centos 6
# this can probably be removed when conda-build gets updated defaults
# for aarch64
- - clhep
- geant4
# Pre-existing conda-forge-pinning inconsistency, unrelated to emscripten:
# clhep (3 entries) and geant4 (1 entry) are zipped together but not the same
# length -- rattler-build's `--render-only` (used to sanity-check variant
# resolution while adding emscripten support) validates every zip_keys group
# up front regardless of whether a given recipe touches it, so this crashes
# `--render-only` on ANY target platform even though neither HEP package is
# ever built by RoboStack and a plain bulk `rattler-build build
# --recipe-dir` never happens to trigger it. Trimmed clhep down to geant4's
# single already-selected version so the group is internally consistent.
cdt_arch: armv7l # [armv7l]
BUILD: armv7-conda_cos7-linux-gnueabihf # [armv7l]

Expand Down Expand Up @@ -294,8 +304,6 @@ ccr:
cfitsio:
- 4.6.4
clhep:
- 2.4.4.0
- 2.4.7.1
- 2.4.7.2
cmocka:
- 2.0.1
Expand Down Expand Up @@ -433,16 +441,12 @@ googleapis_cpp:
gpgme:
- '1.24'
graphviz:
- '14'
# Harfbuzz guarantees total ABI compatibiblity
# The first version to have this new ABI pin is 11.0.1
# https://github.com/conda-forge/harfbuzz-feedstock/pull/125
# But as of 2025/08/03, 11.0.1 is quite "old" and it is pretty safe
# to release the pin
# We are leaving this comment here to discourage others from adding
# a harfbuzz global pin, as it is not needed.
# harfbuzz:
# - '11'
- '14' # [not emscripten]
- 15.1.0 # [emscripten]
# emscripten-forge/recipes only publishes libffi 3.4.6 -- our regular
# 3.5.* pin (from conda-forge-pinning) has no candidates there, breaking
# rmw_wasm_cpp's host: dep on it (added via patch/dependencies.yaml
# add_host since its package.xml never declares one).
hepmc2:
- '2.06'
hepmc3:
Expand Down Expand Up @@ -568,7 +572,19 @@ libevent:
libexactreal:
- '4'
libffi:
- '3.5'
- '3.5' # [not emscripten]
- 3.4.6 # [emscripten]
# emscripten-forge/recipes only publishes python 3.13 builds today (their own
# variant.yaml pins python: 3.13.* *_cp313), well behind conda-forge's 3.14 --
# keep 3.14 for every other platform and use their exact pin for emscripten.
# Written as a v1 if/then/else (rather than the legacy `# [selector]`
# comment form used elsewhere in this file) because a cross-compiling
# recipe's BUILD-time deps need to resolve against the *build platform*'s
# own variant (e.g. a native osx-arm64 rosidl_default_generators helper,
# itself built at python 3.14), not the invocation's --target-platform;
# the legacy comment selector only ever sees target_platform, so it pinned
# python 3.13 globally for the whole invocation regardless of which
# sub-resolution was actually being solved.
libflac:
- '1.5'
libflatsurf:
Expand Down Expand Up @@ -976,10 +992,15 @@ pythia8:
python:
# conda-forge supports only 3.14+ for win-arm64 and linux-riscv64
# part of a zip_keys: python, is_python_min
- 3.14.* *_cp314
- 3.13.* *_cp313 # [emscripten]
- 3.14.* *_cp314 # [not emscripten]
python_impl:
- cpython

- cpython # Needed for cross-compiled targets: emitted whenever a recipe's
# `packages_select_by_deps`/host deps expand `cross-python_${{ target_platform }}`
# (see e.g. ros-humble-rosidl-default-generators's host deps) with
# target_platform == emscripten-wasm32. Not a conda-forge-pinning key at all
# (conda-forge doesn't cross-compile to emscripten), so it must be added here
# rather than merely overridden. Matches emscripten-forge/recipes' variant.yaml.
python_min:
# minimum supported python version per CFEP-25
# bump to next minor version when we drop python versions
Expand All @@ -989,7 +1010,8 @@ is_freethreading:
- false
is_python_min:
# part of a zip_keys: python, is_python_min
- false
- false # [emscripten]
- false # [not emscripten]
is_abi3:
- true
pytorch:
Expand Down Expand Up @@ -1170,4 +1192,13 @@ zstd:
libzenohc:
- 1.10.0
libzenohcxx:
- 1.10.0
- 1.10.0 # emscripten-forge/recipes only publishes graphviz 15.1.0 -- our regular
# graphviz 14 pin (from conda-forge-pinning) has no candidates there,
# breaking eigenpy/pinocchio's host: dep on it.
cross-python_emscripten-wasm32:
- 3.13.1 # The emscripten SDK/toolchain package version itself, keyed by vinca's
# `<pkgname>_<target_platform>` convention for cross-compilation toolchain
# packages (mirrors emscripten_emscripten-wasm32 in emscripten-forge/recipes'
# variant.yaml). Keep in sync with c_compiler_version/cxx_compiler_version above.
emscripten_emscripten-wasm32:
- 4.0.9
48 changes: 42 additions & 6 deletions patch/dependencies.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
ament_package:
add_host: ["importlib_resources"]
rmw_wasm_cpp:
# CMakeLists.txt directly calls pkg_check_modules(libffi REQUIRED libffi)
# but package.xml never declares a <depend> for it, so it's never resolved
# into the host env at all (surfaced on wasm32 since cross-python_emscripten-wasm32
# doesn't transitively pull in libffi the way a native "python" package does).
add_host: ["libffi"]
foxglove_bridge:
add_host: ["ros-humble-ament-cmake"]
ros_ign_interfaces:
Expand All @@ -16,11 +22,33 @@ cartographer:
remove_host: ["lua", "gflags"]
remove_run: ["lua", "gflags"]
libyaml_vendor:
add_host: ["yaml-cpp", "yaml"]
add_run: ["yaml-cpp", "yaml"]
# emscripten-forge/recipes publishes the C library under its newer name
# "libyaml" (not the legacy conda-forge "yaml"), and doesn't publish
# "yaml-cpp" at all for wasm32 -- libyaml_vendor's own CMakeLists only
# needs plain libyaml (its Findyaml.cmake falls back to pkg-config's
# "yaml-0.1" module, which the conda "libyaml" package still provides
# regardless of the conda package's own name).
add_host:
- if: wasm32
then: ["libyaml"]
else: ["yaml-cpp", "yaml"]
add_run:
- if: wasm32
then: ["libyaml"]
else: ["yaml-cpp", "yaml"]
yaml_cpp_vendor:
add_host: ["yaml-cpp"]
add_run: ["yaml-cpp"]
# No "yaml-cpp" conda package exists for emscripten-wasm32 (confirmed
# absent from emscripten-forge-4x's repodata, 2026-09-07). Omitting the
# host/run dep for wasm32 makes yaml_cpp_vendor's own
# find_package(yaml-cpp QUIET) fail as designed, so its CMakeLists falls
# back to its original ExternalProject_Add source build instead of
# redirecting to a nonexistent system package.
add_host:
- if: not wasm32
then: ["yaml-cpp"]
add_run:
- if: not wasm32
then: ["yaml-cpp"]
zstd_vendor:
add_host: ["zstd", "${{ 'zstd-static' if not win }}"]
add_run: ["zstd"]
Expand Down Expand Up @@ -222,8 +250,16 @@ rqt_image_overlay:
tracetools:
remove_host: [ "pkg-config" ]
spdlog_vendor:
add_host: ["spdlog"]
add_run: ["spdlog"]
# No spdlog conda package exists for emscripten-wasm32 (same rationale as
# yaml_cpp_vendor above): omit the host/run dep so its own
# find_package(spdlog QUIET) fails as designed and it falls back to its
# ExternalProject source build.
add_host:
- if: not wasm32
then: ["spdlog"]
add_run:
- if: not wasm32
then: ["spdlog"]
rcl_logging_spdlog:
add_host: ["${{ 'fmt' if emscripten }}"]
rcl:
Expand Down
62 changes: 62 additions & 0 deletions patch/ros-humble-rcutils.emscripten.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--- a/src/time_unix.c 2026-09-08 19:31:05
+++ b/src/time_unix.c 2026-09-08 19:52:59
@@ -61,6 +61,23 @@
mach_port_deallocate(mach_task_self(), cclock);
timespec_now.tv_sec = mts.tv_sec;
timespec_now.tv_nsec = mts.tv_nsec;
+#elif defined(__EMSCRIPTEN__)
+ // clock_gettime(CLOCK_REALTIME, ...) silently returns a struct that's
+ // still all-zero (tv_sec=0, tv_nsec=0) here specifically when main() has
+ // been moved onto a real pthread Worker via -s PROXY_TO_PTHREAD=1 (its
+ // return value isn't checked below, matching upstream, so this could
+ // also be a silent failure rather than a genuine epoch-0 result -- either
+ // way the wall-clock value coming out of it is unusable). A timer whose
+ // rcl_clock_get_now() always returns the same value never looks any
+ // closer to its next scheduled firing, so timers created against this
+ // clock (rclcpp::Node's default, used by create_wall_timer() whenever
+ // sim time isn't active) fire exactly once and then never again.
+ // CLOCK_MONOTONIC does not have this problem in the same worker context,
+ // so use it here too -- this trades true wall-clock alignment (logged
+ // timestamps become time-since-worker-start instead of real UTC) for a
+ // clock that actually advances, which is what every caller of "system
+ // time" in this codebase actually needs it for.
+ clock_gettime(CLOCK_MONOTONIC, &timespec_now);
#else // defined(__MACH__)
// Otherwise use clock_gettime.
clock_gettime(CLOCK_REALTIME, &timespec_now);
@@ -90,11 +107,20 @@
timespec_now.tv_nsec = mts.tv_nsec;
#else // defined(__MACH__)
// Otherwise use clock_gettime.
-#if defined(CLOCK_MONOTONIC_RAW)
+ // CLOCK_MONOTONIC_RAW is #define'd by emscripten's headers (so the
+ // defined() check below picks it) but the wasm32/pthread runtime's
+ // actual implementation of it is broken here -- it silently writes back
+ // a bogus timespec (in practice: tv_nsec's value ends up read out where
+ // tv_sec is expected, producing a "now" that looks like tv_nsec * 1e9
+ // nanoseconds instead of the real time), even though plain
+ // CLOCK_MONOTONIC on the exact same build works correctly. Skip the
+ // RAW variant entirely under emscripten rather than trusting whichever
+ // clock ID the SDK headers happen to define.
+#if defined(CLOCK_MONOTONIC_RAW) && !defined(__EMSCRIPTEN__)
clock_gettime(CLOCK_MONOTONIC_RAW, &timespec_now);
-#else // defined(CLOCK_MONOTONIC_RAW)
+#else // defined(CLOCK_MONOTONIC_RAW) && !defined(__EMSCRIPTEN__)
clock_gettime(CLOCK_MONOTONIC, &timespec_now);
-#endif // defined(CLOCK_MONOTONIC_RAW)
+#endif // defined(CLOCK_MONOTONIC_RAW) && !defined(__EMSCRIPTEN__)
#endif // defined(__MACH__)
if (__WOULD_BE_NEGATIVE(timespec_now.tv_sec, timespec_now.tv_nsec)) {
RCUTILS_SET_ERROR_MSG("unexpected negative time");
@@ -110,7 +136,10 @@
RCUTILS_CHECK_ARGUMENT_FOR_NULL(now, RCUTILS_RET_INVALID_ARGUMENT);
struct timespec timespec_now;

-#if defined(CLOCK_MONOTONIC_RAW)
+ // See the matching comment in rcutils_steady_time_now() above: skip
+ // CLOCK_MONOTONIC_RAW under emscripten, it's broken here even though
+ // the headers define it.
+#if defined(CLOCK_MONOTONIC_RAW) && !defined(__EMSCRIPTEN__)
clockid_t monotonic_raw_clock = CLOCK_MONOTONIC_RAW;
#else
clockid_t monotonic_raw_clock = CLOCK_MONOTONIC;
Loading
Loading