From 6cdf34985164094b8dd3868ce86d6ced715bcac8 Mon Sep 17 00:00:00 2001 From: "Murilo M. Marinho" Date: Wed, 30 Sep 2026 20:14:02 +0100 Subject: [PATCH 1/2] Build macOS wheels, use Python 3.10-3.14 everywhere, fix dqrobotics resolution - publish.yml: add macos-latest (arm64) to the matrix and build every platform for Python 3.10-3.14, the versions dqrobotics publishes wheels for. On macOS, Eigen comes from Homebrew and dqrobotics is built from source into the runner's temporary directory; delocate bundles libdqrobotics.dylib into the wheel and tags it for macOS 14.0 (MACOSX_DEPLOYMENT_TARGET). A smoke test installs each wheel in a clean environment and uses SerialManipulatorSimulatorFriendly. - CMakeLists.txt: the dqrobotics headers include , which needs Eigen's include prefix on the search path; it is on Linux and Windows but not on macOS (Homebrew), so expose only Eigen through an include directory in the build tree. - pyproject.toml: require dqrobotics>=26.4.0a7. Only dqrobotics' pre-releases have wheels for Python >= 3.10, and with >=20.4.0 pip does not choose them (No matching distribution found for dqrobotics>=20.4.0). Also Python version and operating system classifiers. - README: PyPI installation and platforms, the macOS build from source, and DQ([1]) in the modeling example (the current dqrobotics rejects DQ(1)). Tested: on macOS (arm64, Python 3.14, Homebrew Eigen 5.0.1, dqrobotics/cpp master) the wheel builds, delocates to macosx_14_0_arm64, installs in a clean environment, and SerialManipulatorSimulatorFriendly, Clock and the examples run; on Ubuntu Noble, docker compose run marinholab_sas_core passes all checks. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 60 +++++++++++++++++++++++++++++++---- CMakeLists.txt | 21 ++++++++++++ README.md | 27 ++++++++++++++-- pyproject.toml | 15 ++++++++- 4 files changed, 114 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 02ee6ec..097cc3e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,8 +13,16 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest] - python-version: ["3.10", "3.11", "3.12"] + # The same Python versions on every platform: the ones dqrobotics + # publishes wheels for (3.10-3.14 on Linux x86_64/aarch64, macOS arm64 + # and Windows). + os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + # macOS: the oldest macOS the wheel supports (the dqrobotics library that + # delocate bundles into it is built with the same target). + env: + MACOSX_DEPLOYMENT_TARGET: "14.0" steps: - uses: actions/checkout@v6 @@ -46,7 +54,10 @@ jobs: sudo ln -s /usr/include/eigen3/Eigen /usr/include/Eigen # ── macOS dependencies ────────────────────────────────────── - # (macOS is not in the matrix yet; kept for when it is added.) + # Eigen from Homebrew, and dqrobotics built from source (there is no + # Homebrew formula) and installed into the runner's temporary directory, + # which the build step passes to CMake. dqrobotics and this package use + # the same Eigen, since Eigen types cross their interface. - name: Install dependencies (macOS) if: runner.os == 'macOS' run: | @@ -55,9 +66,11 @@ jobs: git clone --depth 1 https://github.com/dqrobotics/cpp.git dqrobotics/cpp cmake -S dqrobotics/cpp -B dqrobotics/cpp/build \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH="$(brew --prefix)" + -DCMAKE_PREFIX_PATH="$(brew --prefix)" \ + -DCMAKE_INSTALL_PREFIX="$RUNNER_TEMP/dqrobotics" cmake --build dqrobotics/cpp/build -j - cmake --install dqrobotics/cpp/build --prefix /usr/local + cmake --install dqrobotics/cpp/build + echo "CMAKE_ARGS=-DCMAKE_PREFIX_PATH=$(brew --prefix);$RUNNER_TEMP/dqrobotics" >> "$GITHUB_ENV" # ── Windows dependencies ──────────────────────────────────── # dqrobotics/cpp expects the vcpkg toolchain at C:/vcpkg. @@ -95,7 +108,7 @@ jobs: pip install build python -m build --wheel env: - CMAKE_ARGS: ${{ runner.os == 'Windows' && '-DCMAKE_PREFIX_PATH=C:/dqrobotics' || '' }} + CMAKE_ARGS: ${{ runner.os == 'Windows' && '-DCMAKE_PREFIX_PATH=C:/dqrobotics' || env.CMAKE_ARGS }} # PyPI rejects wheels tagged `linux_x86_64`/`linux_aarch64` (it requires # a PEP 600 `manylinux` tag). auditwheel relinks/bundles as needed and @@ -113,6 +126,41 @@ jobs: auditwheel repair --plat auto -w dist/ dist/*linux_${arch}.whl rm -f dist/*linux_${arch}.whl + # macOS: bundle libdqrobotics.dylib into the wheel (as auditwheel does + # on Linux) and retag it for MACOSX_DEPLOYMENT_TARGET. + - name: Repair wheel (macOS, delocate) + if: runner.os == 'macOS' + run: | + pip install delocate + delocate-wheel -v -w dist/repaired dist/*.whl + rm -f dist/*.whl + mv dist/repaired/*.whl dist/ + rmdir dist/repaired + + # Install the wheel in a clean environment and use it. --no-deps: only + # papers.icra2019 needs marinholab-solvers-qpoases, whose wheels do not + # cover every platform and Python version; dqrobotics comes from its + # pre-releases, the ones with wheels for Python >= 3.10. + - name: Smoke test + shell: bash + run: | + python -m venv smoke + if [ "$RUNNER_OS" = "Windows" ]; then PY=smoke/Scripts/python; else PY=smoke/bin/python; fi + $PY -m pip install --upgrade pip + $PY -m pip install --pre dqrobotics numpy termcolor + $PY -m pip install --no-deps dist/*.whl + cd "$RUNNER_TEMP" + $GITHUB_WORKSPACE/$PY -c " + import numpy as np + from dqrobotics import DQ + from marinholab.sas.core import Clock, SerialManipulatorSimulatorFriendly as SM + arm = SM(offset_before=[DQ([1])] * 3, offset_after=[DQ([1])] * 3, + actuation_types=[SM.ActuationType.RX, SM.ActuationType.RY, SM.ActuationType.RZ]) + q = np.array([0.1, -0.2, 0.3]) + assert np.shape(arm.raw_pose_jacobian(q, 2)) == (8, 3) + print(arm.raw_fkm(q, 2)) + " + - name: Upload wheel artifact uses: actions/upload-artifact@v6 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index d0a94d5..3efc53e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,27 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) set(MARINHO_LAB_SAS_CORE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +# The dqrobotics headers include both and . +# The Eigen3 package provides /include/eigen3, which resolves the first; +# the second needs /include, which is on the default search path on +# Linux (/usr/include) and Windows (the vcpkg include root) but not on macOS +# (Homebrew's /opt/homebrew/include). Rather than adding all of that prefix +# (which could shadow, e.g., the pybind11 submodule with a Homebrew pybind11), +# expose only Eigen: a directory in the build tree holding an "eigen3" link to +# Eigen's include directory. include_directories applies to sas_cpp as well, +# since it is added below. +find_package(Eigen3 REQUIRED) +if(NOT WIN32) + get_target_property(_eigen3_include_dirs Eigen3::Eigen INTERFACE_INCLUDE_DIRECTORIES) + list(GET _eigen3_include_dirs 0 _eigen3_include_dir) + set(_eigen3_shim "${CMAKE_BINARY_DIR}/eigen3_include_shim") + file(MAKE_DIRECTORY "${_eigen3_shim}") + if(NOT EXISTS "${_eigen3_shim}/eigen3") + file(CREATE_LINK "${_eigen3_include_dir}" "${_eigen3_shim}/eigen3" SYMBOLIC) + endif() + include_directories(SYSTEM "${_eigen3_shim}") +endif() + add_subdirectory(submodules/sas_cpp) set(PYBIND11_FINDPYTHON ON) diff --git a/README.md b/README.md index e2bde8e..759eca9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,16 @@ which registers the dual-quaternion types the bindings expose. ## Installation +From PyPI, with wheels for Python 3.10–3.14 on Linux (x86_64, aarch64), macOS +(arm64, 14.0 or later) and Windows (x86_64): + +```bash +pip install marinholab-sas-core +``` + +The wheels match the Python versions that `dqrobotics` publishes wheels for, +in its pre-releases, which the requirement `dqrobotics>=26.4.0a7` selects. + From source: ```bash @@ -51,6 +61,19 @@ sudo add-apt-repository -y ppa:dqrobotics-dev/development sudo apt install libdqrobotics ``` +On macOS, Eigen comes from Homebrew and `dqrobotics` is built from source +(there is no Homebrew formula), with the same Eigen: + +```bash +brew install eigen cmake ninja +git clone --depth 1 https://github.com/dqrobotics/cpp.git dqrobotics-cpp +cmake -S dqrobotics-cpp -B dqrobotics-cpp/build -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$(brew --prefix)" -DCMAKE_INSTALL_PREFIX="$HOME/.local/dqrobotics" +cmake --build dqrobotics-cpp/build -j && cmake --install dqrobotics-cpp/build +CMAKE_ARGS="-DCMAKE_PREFIX_PATH=$(brew --prefix);$HOME/.local/dqrobotics" \ + pip install . --no-build-isolation +``` + ## Usage ```python @@ -95,8 +118,8 @@ from marinholab.sas.core import SerialManipulatorSimulatorFriendly # 3 revolute joints about X, Y, Z with zero per-joint offsets. arm = SerialManipulatorSimulatorFriendly( - offset_before=[DQ(1), DQ(1), DQ(1)], - offset_after=[DQ(1), DQ(1), DQ(1)], + offset_before=[DQ([1]), DQ([1]), DQ([1])], + offset_after=[DQ([1]), DQ([1]), DQ([1])], actuation_types=[ SerialManipulatorSimulatorFriendly.ActuationType.RX, SerialManipulatorSimulatorFriendly.ActuationType.RY, diff --git a/pyproject.toml b/pyproject.toml index 35d66be..fd52f4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,9 +21,22 @@ maintainers = [ {name = "Murilo M. Marinho", email = "murilomarinho@ieee.org"} ] dynamic = ["version"] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", +] dependencies = [ "numpy", - "dqrobotics>=20.4.0", + # Only dqrobotics' pre-releases have wheels for Python >= 3.10; naming a + # pre-release here lets pip choose them (with ">=20.4.0" it does not). + "dqrobotics>=26.4.0a7", "marinholab-solvers-qpoases", "termcolor", ] From c4b11ad61d973308603e67f7b77a4fedd260f96d Mon Sep 17 00:00:00 2001 From: "Murilo M. Marinho" Date: Wed, 30 Sep 2026 20:18:45 +0100 Subject: [PATCH 2/2] Tag the macOS wheels arm64, not universal2 setup-python's python.org builds are universal2, so setuptools tagged the wheel universal2 while the extension (and the dqrobotics it links) is arm64 only, and delocate-wheel failed: Failed to find any binary with the required architecture: 'x86_64'. ARCHFLAGS=-arch arm64 passes the architecture to CMake (setup.py maps it to CMAKE_OSX_ARCHITECTURES) and _PYTHON_HOST_PLATFORM=macosx-14.0-arm64 tags the wheel. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 6 ++++++ =26.4.0a7 | 0 2 files changed, 6 insertions(+) create mode 100644 =26.4.0a7 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 097cc3e..69815b5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -71,6 +71,12 @@ jobs: cmake --build dqrobotics/cpp/build -j cmake --install dqrobotics/cpp/build echo "CMAKE_ARGS=-DCMAKE_PREFIX_PATH=$(brew --prefix);$RUNNER_TEMP/dqrobotics" >> "$GITHUB_ENV" + # The python.org Pythons of setup-python are universal2 builds, so + # setuptools would tag the wheel universal2; the extension and the + # dqrobotics it links are arm64 only. ARCHFLAGS makes setup.py pass + # the architecture to CMake, and _PYTHON_HOST_PLATFORM tags the wheel. + echo "ARCHFLAGS=-arch arm64" >> "$GITHUB_ENV" + echo "_PYTHON_HOST_PLATFORM=macosx-${MACOSX_DEPLOYMENT_TARGET}-arm64" >> "$GITHUB_ENV" # ── Windows dependencies ──────────────────────────────────── # dqrobotics/cpp expects the vcpkg toolchain at C:/vcpkg. diff --git a/=26.4.0a7 b/=26.4.0a7 new file mode 100644 index 0000000..e69de29