Skip to content
Merged
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
111 changes: 111 additions & 0 deletions .github/workflows/build-python-fcl.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# 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.
# -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 &&
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
From: Ludovic Henry <git@ludovic.dev>
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.<dep> 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 <git@ludovic.dev>
---
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
From: Ludovic Henry <git@ludovic.dev>
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<double>) 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 <git@ludovic.dev>
---
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