From 6f8bab080c651ba6a5bc69171d4e5e6c5612a736 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 11 Sep 2026 12:09:23 +0200 Subject: [PATCH 1/6] python-fcl: add build-python-fcl.yml for riscv64 wheels Builds eigen/libccd/octomap/fcl from source via build_dependencies/ install_linux.sh (mirrors upstream's own push.yml), then the Cython extension against them. Widens license-files to ship the vendored fcl/libccd/octomap licences alongside python-fcl's own. --- .github/workflows/build-python-fcl.yml | 108 ++++++++++++++++++ ...iles-to-cover-the-vendored-fcl-libcc.patch | 39 +++++++ 2 files changed, 147 insertions(+) create mode 100644 .github/workflows/build-python-fcl.yml create mode 100644 patches/python-fcl/0.7.0.11/0001-Widen-license-files-to-cover-the-vendored-fcl-libcc.patch diff --git a/.github/workflows/build-python-fcl.yml b/.github/workflows/build-python-fcl.yml new file mode 100644 index 000000000..449d47898 --- /dev/null +++ b/.github/workflows/build-python-fcl.yml @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on: https://github.com/berkeleyautomation/python-fcl/blob/v0.7.0.11/.github/workflows/push.yml +name: Build python-fcl wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'python-fcl version to build (git tag without leading v, e.g. 0.7.0.11)' + required: true + default: '0.7.0.11' + pull_request: + paths: + - '.github/workflows/build-python-fcl.yml' + - 'patches/python-fcl/**' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '0.7.0.11' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # `inputs.version` is empty on pull_request events; default to 0.7.0.11 there. + PYTHON_FCL_VERSION: ${{ inputs.version || '0.7.0.11' }} + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + build_wheels: + needs: [setup] + name: Build python-fcl ${{ inputs.version || '0.7.0.11' }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + python: + - "cp312" + - "cp313" + - "cp314" + - "cp314t" + + steps: + - name: Checkout python-fcl v${{ env.PYTHON_FCL_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: berkeleyautomation/python-fcl + ref: v${{ env.PYTHON_FCL_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Apply patches + run: git apply -v python-wheels/patches/python-fcl/${{ env.PYTHON_FCL_VERSION }}/*.patch + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # build_dependencies/install_linux.sh builds eigen/libccd/octomap/fcl from + # source; the license copies are appended here since CIBW_BEFORE_ALL_LINUX + # fully replaces pyproject.toml's own before-all rather than extending it. + CIBW_BEFORE_ALL_LINUX: >- + bash build_dependencies/install_linux.sh && + cp deps/fcl/LICENSE /project/LICENSE.fcl && + cp deps/libccd/BSD-LICENSE /project/LICENSE.libccd && + cp deps/octomap/octomap/LICENSE.txt /project/LICENSE.octomap + # numpy: our registry is one patch release behind PyPI's latest (gotcha 84). + # cmake: install_linux.sh pip-installs an exact-pinned cmake==3.31.6, which + # our registry also carries; PIP_ONLY_BINARY keeps it off a source bootstrap. + CIBW_ENVIRONMENT: >- + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + PIP_ONLY_BINARY=cmake,numpy + CIBW_TEST_REQUIRES: pytest + CIBW_TEST_COMMAND: >- + python -c "import importlib.metadata as m; + n={p.name for p in m.files('python-fcl') if '.dist-info/licenses/' in str(p)}; + assert n == {'LICENSE', 'LICENSE.fcl', 'LICENSE.libccd', 'LICENSE.octomap'}, n" && + pytest {project}/tests + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: python-fcl-${{ env.PYTHON_FCL_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish python-fcl ${{ inputs.version || '0.7.0.11' }} + needs: [setup, build_wheels] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: python-fcl-${{ inputs.version || '0.7.0.11' }}-*-manylinux_riscv64 diff --git a/patches/python-fcl/0.7.0.11/0001-Widen-license-files-to-cover-the-vendored-fcl-libcc.patch b/patches/python-fcl/0.7.0.11/0001-Widen-license-files-to-cover-the-vendored-fcl-libcc.patch new file mode 100644 index 000000000..4c57f7f64 --- /dev/null +++ b/patches/python-fcl/0.7.0.11/0001-Widen-license-files-to-cover-the-vendored-fcl-libcc.patch @@ -0,0 +1,39 @@ +From: Ludovic Henry +Date: Fri, 11 Sep 2026 10:00:00 +0000 +Subject: [PATCH] Widen license-files to cover the vendored fcl/libccd/octomap licences + +pyproject.toml's explicit license-files list only names LICENSE, so +setuptools' default LICEN[CS]E* glob never applies here and a dropped-in +LICENSE. is silently ignored. build_dependencies/install_linux.sh +builds fcl, libccd and octomap from source and auditwheel vendors their +shared libraries into the wheel (confirmed on the official +python_fcl-0.7.0.11 manylinux_x86_64 wheel on PyPI, which ships +libfcl/libccd/liboctomap/liboctomath but none of their licence texts), +so every platform's wheel is missing them, not just riscv64's. + +Widening the list to LICENSE.* lets LICENSE.fcl, LICENSE.libccd and +LICENSE.octomap, staged at the project root during the build, reach +dist-info/licenses/ with no other packaging change. + +Upstream-Status: To upstream [not yet submitted; the same gap exists in every python-fcl wheel on PyPI regardless of architecture, so this needs a maintainer discussion rather than a drive-by PR] + +Signed-off-by: Ludovic Henry +--- + pyproject.toml | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/pyproject.toml b/pyproject.toml +index 30750c6..b46aa56 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -55,7 +55,7 @@ Homepage = "https://github.com/berkeleyautomation/python-fcl" + [tool.setuptools] + include-package-data = true + package-dir = {"" = "src"} +-license-files = ["LICENSE"] ++license-files = ["LICENSE", "LICENSE.*"] + + [tool.setuptools.package-data] + "*" = ["*.pyx", "*.pxd", "*.dll"] +-- +2.50.1 From 799162e848732a2099dff355f7616c393a37e148 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 11 Sep 2026 13:30:23 +0200 Subject: [PATCH 2/6] python-fcl: diagnose riscv64-only undefined-symbol failure (WIP) All 4 matrix legs on PR #1770 failed identically at import with 'undefined symbol: typeinfo for fcl::CollisionGeometry' (_ZTIN3fcl17CollisionGeometryIdEE). fcl's own CMake config sets no -fvisibility, and Debian's riscv64 buildd builds+tests the same upstream FCL 0.7.0 successfully, so this isn't a compiler/arch RTTI limitation. auditwheel's repair log shows zero libraries vendored (only a platform-tag relabel), unlike the official x86_64 wheel, which points at a link-time/dependency-resolution gap in our build rather than fcl's source. Trims the matrix to cp312 and adds before- build diagnostics (ldd + nm -D on the freshly built libfcl.so) to confirm whether the symbol is exported from the library itself. Also skips fcl's own ~25min test-binary build (BUILD_TESTING=OFF, not shipped in the wheel) to speed up iteration. --- .github/workflows/build-python-fcl.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-python-fcl.yml b/.github/workflows/build-python-fcl.yml index 449d47898..e2c8d4bdf 100644 --- a/.github/workflows/build-python-fcl.yml +++ b/.github/workflows/build-python-fcl.yml @@ -42,9 +42,6 @@ jobs: matrix: python: - "cp312" - - "cp313" - - "cp314" - - "cp314t" steps: - name: Checkout python-fcl v${{ env.PYTHON_FCL_VERSION }} @@ -73,7 +70,12 @@ jobs: # build_dependencies/install_linux.sh builds eigen/libccd/octomap/fcl from # source; the license copies are appended here since CIBW_BEFORE_ALL_LINUX # fully replaces pyproject.toml's own before-all rather than extending it. + # DEBUG: -DBUILD_TESTING=OFF skips fcl's own ~25min test-binary build (not + # shipped in the wheel either way) to speed up iteration while diagnosing + # the riscv64-only "undefined symbol: typeinfo for fcl::CollisionGeometry" + # failure seen on PR #1770 (all 4 legs, same symbol every time). CIBW_BEFORE_ALL_LINUX: >- + sed -i '/^cd fcl$/,/^cd \.\.$/ s/^\$CMAKE \.[[:space:]]*$/$CMAKE . -DBUILD_TESTING=OFF/' build_dependencies/install_linux.sh && bash build_dependencies/install_linux.sh && cp deps/fcl/LICENSE /project/LICENSE.fcl && cp deps/libccd/BSD-LICENSE /project/LICENSE.libccd && @@ -84,6 +86,17 @@ jobs: CIBW_ENVIRONMENT: >- PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ PIP_ONLY_BINARY=cmake,numpy + # DEBUG: dump libfcl.so's own dynamic symbol table to confirm whether the + # explicit `template class CollisionGeometry;` instantiation + # (src/geometry/collision_geometry.cpp) is actually exported, before + # setup.py links the Cython extension against it. + CIBW_BEFORE_BUILD: >- + echo ---LDD--- ; + ldd /usr/local/lib64/libfcl.so ; + echo ---DEFINED--- ; + nm -D --defined-only /usr/local/lib64/libfcl.so | grep -i CollisionGeometry ; + echo ---DONE--- ; + true CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: >- python -c "import importlib.metadata as m; From 4982f582c78db510f1f48f813dc8e0128f54bc55 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 11 Sep 2026 14:08:25 +0200 Subject: [PATCH 3/6] python-fcl: introspect the extension .so directly (WIP diagnosis) before-build confirmed libfcl.so exports the missing symbol (_ZTIN3fcl17CollisionGeometryIdEE, weak+global, verified via nm -D) and ldd resolves all of libfcl.so's own deps cleanly, so the gap is between the built Cython extension and that export, not the library. Dumps the extension .so's own ldd/NEEDED/RUNPATH/undefined-symbol state via find_spec (no import, so it doesn't trip the crash) before the pytest run that's expected to still fail. --- .github/workflows/build-python-fcl.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build-python-fcl.yml b/.github/workflows/build-python-fcl.yml index e2c8d4bdf..09d74ba4d 100644 --- a/.github/workflows/build-python-fcl.yml +++ b/.github/workflows/build-python-fcl.yml @@ -98,10 +98,20 @@ jobs: echo ---DONE--- ; true CIBW_TEST_REQUIRES: pytest + # DEBUG: libfcl.so itself exports _ZTIN3fcl17CollisionGeometryIdEE (weak, + # global, confirmed via before-build's nm -D) so the gap is between the + # installed extension and that export; introspect the extension .so + # directly (find_spec locates it without importing/triggering the crash). CIBW_TEST_COMMAND: >- python -c "import importlib.metadata as m; n={p.name for p in m.files('python-fcl') if '.dist-info/licenses/' in str(p)}; assert n == {'LICENSE', 'LICENSE.fcl', 'LICENSE.libccd', 'LICENSE.octomap'}, n" && + SO=$(python -c "import importlib.util as u; print(u.find_spec('fcl.fcl').origin)") && + echo ---SO-PATH--- && echo $SO && + echo ---LDD--- && ldd $SO && + echo ---NEEDED--- && readelf -d $SO | grep -i NEEDED && + echo ---RUNPATH--- && readelf -d $SO | grep -i PATH && + echo ---UNDEFINED--- && nm -D --undefined-only $SO | grep -i CollisionGeometry ; pytest {project}/tests - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 From b9ea1621d2f6e5e26e71e8b0caa5589484f09941 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 11 Sep 2026 14:39:43 +0200 Subject: [PATCH 4/6] python-fcl: locate the extension .so via dist-info metadata (WIP) find_spec('fcl.fcl') still imports the fcl package first (to locate the submodule), tripping the same crash with none of the intended diagnostics printed. importlib.metadata.distribution(...).files lists the RECORD without importing anything, so use that to find the .so path instead. --- .github/workflows/build-python-fcl.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-python-fcl.yml b/.github/workflows/build-python-fcl.yml index 09d74ba4d..e0f2bb996 100644 --- a/.github/workflows/build-python-fcl.yml +++ b/.github/workflows/build-python-fcl.yml @@ -100,13 +100,17 @@ jobs: CIBW_TEST_REQUIRES: pytest # DEBUG: libfcl.so itself exports _ZTIN3fcl17CollisionGeometryIdEE (weak, # global, confirmed via before-build's nm -D) so the gap is between the - # installed extension and that export; introspect the extension .so - # directly (find_spec locates it without importing/triggering the crash). + # installed extension and that export. `find_spec('fcl.fcl')` still runs + # fcl/__init__.py (imports the parent package first) and trips the same + # crash with zero diagnostic output, so locate the .so via dist-info + # RECORD metadata instead, which needs no import at all. CIBW_TEST_COMMAND: >- - python -c "import importlib.metadata as m; - n={p.name for p in m.files('python-fcl') if '.dist-info/licenses/' in str(p)}; - assert n == {'LICENSE', 'LICENSE.fcl', 'LICENSE.libccd', 'LICENSE.octomap'}, n" && - SO=$(python -c "import importlib.util as u; print(u.find_spec('fcl.fcl').origin)") && + SO=$(python -c "import importlib.metadata as m; + d=m.distribution('python-fcl'); files=list(d.files); + lic={str(f).rsplit('/',1)[-1] for f in files if '.dist-info/licenses/' in str(f)}; + assert lic=={'LICENSE','LICENSE.fcl','LICENSE.libccd','LICENSE.octomap'}, lic; + so=[f for f in files if str(f).endswith('.so')]; assert len(so)==1, so; + print(d.locate_file(so[0]))") && echo ---SO-PATH--- && echo $SO && echo ---LDD--- && ldd $SO && echo ---NEEDED--- && readelf -d $SO | grep -i NEEDED && From 610ff2f6c1cc73c6d468c13aae745e14b8e23929 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 11 Sep 2026 15:11:59 +0200 Subject: [PATCH 5/6] python-fcl: force -Wl,--no-as-needed to keep libfcl.so on the link line readelf -d on the built extension shows liboctomap correctly NEEDED and vendored, but libfcl.so is entirely absent from NEEDED despite being requested via setup.py's libraries=["fcl", "octomap"] and despite the extension having genuine unresolved references into it (nm -D --undefined-only lists exactly fcl::CollisionGeometry's ctor/dtor/typeinfo/vtable, which only exist via fcl's own `extern template ... FCL_EXPORT CollisionGeometry` pattern). libfcl.so does export those symbols (weak+global, confirmed via before-build's nm -D --defined-only), so this looks like the image's --as-needed linker default dropping -lfcl. LDFLAGS=-Wl,--no-as-needed forces every requested -l onto NEEDED regardless. --- .github/workflows/build-python-fcl.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build-python-fcl.yml b/.github/workflows/build-python-fcl.yml index e0f2bb996..f377774ce 100644 --- a/.github/workflows/build-python-fcl.yml +++ b/.github/workflows/build-python-fcl.yml @@ -83,9 +83,20 @@ jobs: # numpy: our registry is one patch release behind PyPI's latest (gotcha 84). # cmake: install_linux.sh pip-installs an exact-pinned cmake==3.31.6, which # our registry also carries; PIP_ONLY_BINARY keeps it off a source bootstrap. + # DEBUG: the extension links cleanly against liboctomap (also in setup.py's + # `libraries=`) but libfcl.so is entirely absent from the extension's own + # NEEDED list even though fcl.pyx has genuine unresolved references into it + # (confirmed: nm -D --undefined-only on the built .so lists exactly the + # CollisionGeometry ctor/dtor/typeinfo/vtable symbols that only + # exist via fcl's `extern template ... FCL_EXPORT CollisionGeometry`, + # and libfcl.so itself DOES export them, weak+global, per before-build's + # nm -D --defined-only). The image's default linker behaves as --as-needed; + # force it off so every requested -l lands in NEEDED regardless of ld's own + # (seemingly wrong here) per-library usage analysis. CIBW_ENVIRONMENT: >- PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ PIP_ONLY_BINARY=cmake,numpy + LDFLAGS=-Wl,--no-as-needed # DEBUG: dump libfcl.so's own dynamic symbol table to confirm whether the # explicit `template class CollisionGeometry;` instantiation # (src/geometry/collision_geometry.cpp) is actually exported, before From 5c86716cbfd5d71e092388dbb88be4011fdb2e7e Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Fri, 11 Sep 2026 16:06:23 +0200 Subject: [PATCH 6/6] python-fcl: fix the real bug (empty LD_LIBRARY_PATH breaks -lfcl) and clean up setup.py's get_libraries_dir() checks "LD_LIBRARY_PATH" in os.environ and unconditionally splits its value; our build container exports it empty (present, not unset), so "".split(":") appends an empty string to library_dirs. distutils turns that into a bare -L with no argument, which swallows the following -lfcl as -L's path instead of a library request (confirmed via the actual printed link command: '-L -lfcl'). -loctomap, being last on the line, is unaffected - matching the asymmetry seen in the wheel (liboctomap present and vendored, libfcl entirely absent from NEEDED). That's the real source of the 'undefined symbol: typeinfo for fcl::CollisionGeometry' failure on every interpreter; it has nothing to do with riscv64's toolchain or --as-needed (confirmed: forcing -Wl,--no-as-needed in 610ff2f6c made no difference, since -lfcl was never parsed as a library flag to begin with). 0002-*.patch guards get_libraries_dir() (and the identical CPATH pattern in get_include_dirs()) against an empty-but-present value. Drops the now-unneeded LDFLAGS override and the debug-only before-build/test-command introspection, and restores the full cp312/cp313/cp314/cp314t matrix. --- .github/workflows/build-python-fcl.yml | 51 +++----------- ...n-empty-but-present-LD_LIBRARY_PATH-.patch | 68 +++++++++++++++++++ 2 files changed, 76 insertions(+), 43 deletions(-) create mode 100644 patches/python-fcl/0.7.0.11/0002-Guard-against-an-empty-but-present-LD_LIBRARY_PATH-.patch diff --git a/.github/workflows/build-python-fcl.yml b/.github/workflows/build-python-fcl.yml index f377774ce..ea9d9e27a 100644 --- a/.github/workflows/build-python-fcl.yml +++ b/.github/workflows/build-python-fcl.yml @@ -42,6 +42,9 @@ jobs: matrix: python: - "cp312" + - "cp313" + - "cp314" + - "cp314t" steps: - name: Checkout python-fcl v${{ env.PYTHON_FCL_VERSION }} @@ -70,10 +73,8 @@ jobs: # build_dependencies/install_linux.sh builds eigen/libccd/octomap/fcl from # source; the license copies are appended here since CIBW_BEFORE_ALL_LINUX # fully replaces pyproject.toml's own before-all rather than extending it. - # DEBUG: -DBUILD_TESTING=OFF skips fcl's own ~25min test-binary build (not - # shipped in the wheel either way) to speed up iteration while diagnosing - # the riscv64-only "undefined symbol: typeinfo for fcl::CollisionGeometry" - # failure seen on PR #1770 (all 4 legs, same symbol every time). + # -DBUILD_TESTING=OFF skips fcl's own test-binary build (not shipped in the + # wheel either way, and it otherwise adds ~25min per interpreter). CIBW_BEFORE_ALL_LINUX: >- sed -i '/^cd fcl$/,/^cd \.\.$/ s/^\$CMAKE \.[[:space:]]*$/$CMAKE . -DBUILD_TESTING=OFF/' build_dependencies/install_linux.sh && bash build_dependencies/install_linux.sh && @@ -83,50 +84,14 @@ jobs: # numpy: our registry is one patch release behind PyPI's latest (gotcha 84). # cmake: install_linux.sh pip-installs an exact-pinned cmake==3.31.6, which # our registry also carries; PIP_ONLY_BINARY keeps it off a source bootstrap. - # DEBUG: the extension links cleanly against liboctomap (also in setup.py's - # `libraries=`) but libfcl.so is entirely absent from the extension's own - # NEEDED list even though fcl.pyx has genuine unresolved references into it - # (confirmed: nm -D --undefined-only on the built .so lists exactly the - # CollisionGeometry ctor/dtor/typeinfo/vtable symbols that only - # exist via fcl's `extern template ... FCL_EXPORT CollisionGeometry`, - # and libfcl.so itself DOES export them, weak+global, per before-build's - # nm -D --defined-only). The image's default linker behaves as --as-needed; - # force it off so every requested -l lands in NEEDED regardless of ld's own - # (seemingly wrong here) per-library usage analysis. CIBW_ENVIRONMENT: >- PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ PIP_ONLY_BINARY=cmake,numpy - LDFLAGS=-Wl,--no-as-needed - # DEBUG: dump libfcl.so's own dynamic symbol table to confirm whether the - # explicit `template class CollisionGeometry;` instantiation - # (src/geometry/collision_geometry.cpp) is actually exported, before - # setup.py links the Cython extension against it. - CIBW_BEFORE_BUILD: >- - echo ---LDD--- ; - ldd /usr/local/lib64/libfcl.so ; - echo ---DEFINED--- ; - nm -D --defined-only /usr/local/lib64/libfcl.so | grep -i CollisionGeometry ; - echo ---DONE--- ; - true CIBW_TEST_REQUIRES: pytest - # DEBUG: libfcl.so itself exports _ZTIN3fcl17CollisionGeometryIdEE (weak, - # global, confirmed via before-build's nm -D) so the gap is between the - # installed extension and that export. `find_spec('fcl.fcl')` still runs - # fcl/__init__.py (imports the parent package first) and trips the same - # crash with zero diagnostic output, so locate the .so via dist-info - # RECORD metadata instead, which needs no import at all. CIBW_TEST_COMMAND: >- - SO=$(python -c "import importlib.metadata as m; - d=m.distribution('python-fcl'); files=list(d.files); - lic={str(f).rsplit('/',1)[-1] for f in files if '.dist-info/licenses/' in str(f)}; - assert lic=={'LICENSE','LICENSE.fcl','LICENSE.libccd','LICENSE.octomap'}, lic; - so=[f for f in files if str(f).endswith('.so')]; assert len(so)==1, so; - print(d.locate_file(so[0]))") && - echo ---SO-PATH--- && echo $SO && - echo ---LDD--- && ldd $SO && - echo ---NEEDED--- && readelf -d $SO | grep -i NEEDED && - echo ---RUNPATH--- && readelf -d $SO | grep -i PATH && - echo ---UNDEFINED--- && nm -D --undefined-only $SO | grep -i CollisionGeometry ; + python -c "import importlib.metadata as m; + n={p.name for p in m.files('python-fcl') if '.dist-info/licenses/' in str(p)}; + assert n == {'LICENSE', 'LICENSE.fcl', 'LICENSE.libccd', 'LICENSE.octomap'}, n" && pytest {project}/tests - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/patches/python-fcl/0.7.0.11/0002-Guard-against-an-empty-but-present-LD_LIBRARY_PATH-.patch b/patches/python-fcl/0.7.0.11/0002-Guard-against-an-empty-but-present-LD_LIBRARY_PATH-.patch new file mode 100644 index 000000000..ffae1bac9 --- /dev/null +++ b/patches/python-fcl/0.7.0.11/0002-Guard-against-an-empty-but-present-LD_LIBRARY_PATH-.patch @@ -0,0 +1,68 @@ +From: Ludovic Henry +Date: Fri, 11 Sep 2026 14:20:00 +0000 +Subject: [PATCH] Guard against an empty-but-present LD_LIBRARY_PATH/CPATH in setup.py + +get_libraries_dir() checks `"LD_LIBRARY_PATH" in os.environ` and, if +true, appends `os.environ["LD_LIBRARY_PATH"].split(":")` to +library_dirs regardless of whether the value is actually empty. Our +build container exports LD_LIBRARY_PATH="" (present, empty), so +"".split(":") yields [''], and that empty string becomes a bare `-L` +with no path argument on the link command line: + + g++ ... -L/usr/lib -L/usr/local/lib -L/usr/lib64 -L/usr/local/lib64 -L -lfcl -loctomap -o fcl.cpython-312-riscv64-linux-gnu.so + +`-L` with no following value swallows the *next* argv as its path +(ld/gcc treat "-L -lfcl" as one directory-search option whose value is +the literal string "-lfcl"), so -lfcl is consumed there instead of +being parsed as a request to link libfcl. -loctomap, being the last +argument, is unaffected and links normally - which is exactly the +asymmetry seen in the wheel's dynamic section (liboctomap present, +libfcl entirely absent from NEEDED, confirmed via readelf -d), and +why the built extension carries unresolved references into libfcl.so +(e.g. typeinfo for fcl::CollisionGeometry) that nothing ever +supplies, surfacing as an ImportError: undefined symbol at import +time. Confirmed to reproduce identically with -Wl,--no-as-needed +forced on - the flag has nothing to act on, since -lfcl was never +parsed as a library request in the first place. + +get_include_dirs()'s CPATH check has the identical pattern; guarded +the same way for consistency, though our environment did not exhibit +a matching CPATH-triggered bug. + +Upstream-Status: To upstream [not yet submitted; only reproduces when LD_LIBRARY_PATH is exported empty, which our manylinux_riscv64 build container does but upstream's own CI apparently does not] + +Signed-off-by: Ludovic Henry +--- + setup.py | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/setup.py b/setup.py +index 9ebe01f..9d52455 100644 +--- a/setup.py ++++ b/setup.py +@@ -23,8 +23,9 @@ def get_include_dirs(): + "/usr/local/include/eigen3", + ] + +- if "CPATH" in os.environ: +- include_dirs += os.environ["CPATH"].split(":") ++ cpath = os.environ.get("CPATH", "") ++ if cpath: ++ include_dirs += [p for p in cpath.split(":") if p] + + elif sys.platform == "win32": + include_dirs = [ +@@ -45,8 +46,9 @@ def get_libraries_dir(): + if is_nix_platform(sys.platform): + lib_dirs = ["/usr/lib", "/usr/local/lib", "/usr/lib64", "/usr/local/lib64"] + +- if "LD_LIBRARY_PATH" in os.environ: +- lib_dirs += os.environ["LD_LIBRARY_PATH"].split(":") ++ ld_library_path = os.environ.get("LD_LIBRARY_PATH", "") ++ if ld_library_path: ++ lib_dirs += [p for p in ld_library_path.split(":") if p] + return lib_dirs + if sys.platform == "win32": + return [f"{INSTALL_PREFIX_WIN}\\lib"] +-- +2.50.1