Skip to content

Commit

Permalink
Add support for building Python wheels for Linux & MacOS (#1103)
Browse files Browse the repository at this point in the history
* Modify native build for better MacOS support.

* Add wheel generation scripts and CI jobs.

* Don't remove ompl.util import from ompl.base.

* Don't delete tag every time.

* Only tag prerelease if on main branch.

* Don't auto-update brew.

* Specify pygccxml==2.2.1.

* Build all the Python versions!
  • Loading branch information
kylc committed Nov 30, 2023
1 parent 07561fd commit df058b7
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 13 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/before_all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash

set -eux

build_os="$(uname)"

if [ "${build_os}" == "Linux" ]; then
yum -y install \
sudo \
eigen3 \
llvm-devel \
clang-devel

# manylinux ships with a pypy installation. Make it available on the $PATH
# so the OMPL build process picks it up and can make use of it during the
# Python binding generation stage.
ln -s /opt/python/pp310-pypy310_pp73/bin/pypy /usr/bin
elif [ "${build_os}" == "Darwin" ]; then
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1
export HOMEBREW_NO_AUTO_UPDATE=1
brew install \
eigen \
pypy3 \
castxml \
llvm@16
fi
73 changes: 73 additions & 0 deletions .github/workflows/before_build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash

set -eux

# Dependency versions.
castxml_version="0.6.2" # version specifier for Linux only
boost_version="1.83.0"

# Collect some information about the build target.
build_os="$(uname)"
python_version=$(python3 -c 'import sys; v=sys.version_info; print(f"{v.major}.{v.minor}")')

install_boost() {
b2_args=("$@")

curl -L "https://boostorg.jfrog.io/artifactory/main/release/${boost_version}/source/boost_${boost_version//./_}.tar.bz2" | tar xj
pushd "boost_${boost_version//./_}"

# Tell boost-python the exact Python install to use, since we may have
# multiple on the host system.
python_include_path=$(python3 -c "from sysconfig import get_paths as gp; print(gp()['include'])")
echo "using python : ${python_version} : : ${python_include_path} ;" > "$HOME/user-config.jam"

./bootstrap.sh
sudo ./b2 "${b2_args[@]}" \
--with-serialization \
--with-filesystem \
--with-system \
--with-program_options \
--with-python \
install

popd
}

install_castxml() {
curl -L "https://github.com/CastXML/CastXML/archive/refs/tags/v${castxml_version}.tar.gz" | tar xz

pushd "CastXML-${castxml_version}"
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
make install
popd
}


# Work inside a temporary directory.
cd "$(mktemp -d -t 'ompl-wheels.XXX')"

if [ "${build_os}" == "Linux" ]; then
# Install CastXML dependency from source, since the manylinux container
# doesn't have a prebuilt version in the repos.
install_castxml

# Install the latest Boost, because it has to be linked to the exact version of
# Python for which we are building the wheel.
install_boost
elif [ "${build_os}" == "Darwin" ]; then
# On MacOS, we may be cross-compiling for a different architecture. Detect
# that here.
build_arch="${OMPL_BUILD_ARCH:-x86_64}"

# Make sure we install the target Python version from brew instead of
# depending on the system version.
brew install --overwrite "python@${python_version}"

if [ "${build_arch}" == "x86_64" ]; then
install_boost architecture=x86 address-model=64 cxxflags="-arch x86_64"
elif [ "${build_arch}" == "arm64" ]; then
install_boost architecture=arm address-model=64 cxxflags="-arch arm64"
fi
fi
58 changes: 58 additions & 0 deletions .github/workflows/wheels.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Build Wheels
on: push

jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}-${{ matrix.arch }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, macos-13]
arch: [x86_64]
include:
- os: macos-13
arch: arm64
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Build wheels
uses: pypa/cibuildwheel@v2.16.1
with:
package-dir: py-bindings
env:
CIBW_ARCHS_MACOS: ${{ matrix.arch }}
OMPL_BUILD_ARCH: ${{ matrix.arch }}
# NOTE: Many combinations of OS, arch, and Python version can be built
# depending on your patience. For example:
CIBW_BUILD: cp3{10,11,12}-macosx_{x86_64,arm64} cp3{7,8,9,10,11,12}-manylinux_x86_64
CIBW_BUILD_VERBOSITY: 1
- uses: actions/upload-artifact@v3
with:
name: wheels
path: wheelhouse

prerelease:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
concurrency:
group: push-${{ github.ref_name }}-prerelease
cancel-in-progress: true
needs: [build_wheels]
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
path: wheelhouse

- name: GitHub release
uses: ncipollo/release-action@v1.12.0
with:
prerelease: true
tag: "prerelease"
name: "Development Build"
allowUpdates: true
removeArtifacts: true
replacesArtifacts: true
artifacts: "wheelhouse/*"
3 changes: 0 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ include(FeatureSummary)
include(CompilerSettings)
include(OMPLUtils)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib")

set(OMPL_CMAKE_UTIL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules"
CACHE FILEPATH "Path to directory with auxiliary CMake scripts for OMPL")
set(OMPL_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/src;${CMAKE_CURRENT_BINARY_DIR}/src")
Expand Down
19 changes: 17 additions & 2 deletions CMakeModules/PythonBindingsUtils.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,24 @@ function(create_module_target module)
target_link_libraries(py_ompl_${module}
ompl
${_extra_libs}
${Boost_PYTHON_LIBRARY}
${PYTHON_LIBRARIES})
${Boost_PYTHON_LIBRARY})
add_dependencies(py_ompl py_ompl_${module})

if(CMAKE_CXX_COMPILER_ID MATCHES "^(Apple)?Clang$")
# This supresses linker errors caused by not dynamically linking to
# libpython. Linux doesn't seem to care, but Apple complains without
# these flags.
set_target_properties(py_ompl_${module}
PROPERTIES LINK_FLAGS
"-undefined suppress -flat_namespace")

# std::unary_function and std::binary_function are removed in XCode
# 12.4 with C++17 and C++20. They are needed to compile the Python
# modules, and can be brought back with this #define.
target_compile_definitions(py_ompl_${module}
PUBLIC _LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION=1)
endif()

if(WIN32)
add_custom_command(TARGET py_ompl_${module} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:py_ompl_${module}>"
Expand Down
21 changes: 21 additions & 0 deletions py-bindings/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[build-system]
requires = [
"setuptools>=42",
"cmake>=3.18",
"ninja",
"pygccxml==2.2.1",
"numpy",
# Need latest commit for this PR with Mac fixes:
# https://github.com/ompl/pyplusplus/pull/1
"pyplusplus @ git+https://github.com/ompl/pyplusplus",
]
build-backend = "setuptools.build_meta"

[tool.cibuildwheel]
archs = ["auto"]
build-verbosity = 1

before-all = ".github/workflows/before_all.sh"
before-build = ".github/workflows/before_build.sh"

manylinux-x86_64-image = "quay.io/pypa/manylinux_2_28_x86_64"
171 changes: 163 additions & 8 deletions py-bindings/setup.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,168 @@
#!/usr/bin/env python

from distutils.core import setup
import os
import re
import subprocess
import sys
import site
from pathlib import Path
from sysconfig import get_paths

from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext

# Convert distutils Windows platform specifiers to CMake -A arguments
PLAT_TO_CMAKE = {
"win32": "Win32",
"win-amd64": "x64",
"win-arm32": "ARM",
"win-arm64": "ARM64",
}


# A CMakeExtension needs a sourcedir instead of a file list.
# The name must be the _single_ output extension from the CMake build.
# If you need multiple extensions, see scikit-build.
class CMakeExtension(Extension):
def __init__(self, name: str, sourcedir: str = "") -> None:
super().__init__(name, sources=[])
self.sourcedir = os.fspath(Path(sourcedir).resolve())


class CMakeBuild(build_ext):
def build_extension(self, ext: CMakeExtension) -> None:
# Must be in this form due to bug in .resolve() only fixed in Python 3.10+
ext_fullpath = Path.cwd() / self.get_ext_fullpath(ext.name)
extdir = ext_fullpath.parent.resolve()

# Using this requires trailing slash for auto-detection & inclusion of
# auxiliary "native" libs

debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug
cfg = "Debug" if debug else "Release"

# CMake lets you override the generator - we need to check this.
# Can be set with Conda-Build, for example.
cmake_generator = os.environ.get("CMAKE_GENERATOR", "")

# Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON
# EXAMPLE_VERSION_INFO shows you how to pass a value into the C++ code
# from Python.
cmake_args = [
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}",
f"-DPYTHON_EXEC={sys.executable}",
f"-DPYTHON_INCLUDE_DIRS={get_paths()['include']}",
f"-DPYTHON_LIBRARIES={get_paths()['stdlib']}",
f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm
"-DOMPL_BUILD_PYBINDINGS=ON",
"-DOMPL_REGISTRATION=OFF",
"-DOMPL_BUILD_DEMOS=OFF",
"-DOMPL_BUILD_PYTESTS=OFF",
"-DOMPL_BUILD_TESTS=OFF",
]
build_args = []
# Adding CMake arguments set as environment variable
# (needed e.g. to build for ARM OSx on conda-forge)
if "CMAKE_ARGS" in os.environ:
cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item]

if self.compiler.compiler_type != "msvc":
# Using Ninja-build since it a) is available as a wheel and b)
# multithreads automatically. MSVC would require all variables be
# exported for Ninja to pick it up, which is a little tricky to do.
# Users can override the generator with CMAKE_GENERATOR in CMake
# 3.15+.
if not cmake_generator or cmake_generator == "Ninja":
try:
import ninja

ninja_executable_path = Path(ninja.BIN_DIR) / "ninja"
cmake_args += [
"-GNinja",
f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}",
]
except ImportError:
pass

else:
# Single config generators are handled "normally"
single_config = any(x in cmake_generator for x in {"NMake", "Ninja"})

# CMake allows an arch-in-generator style for backward compatibility
contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"})

# Specify the arch if using MSVC generator, but only if it doesn't
# contain a backward-compatibility arch spec already in the
# generator name.
if not single_config and not contains_arch:
cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]]

# Multi-config generators have a different way to specify configs
if not single_config:
cmake_args += [
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}"
]
build_args += ["--config", cfg]

if sys.platform.startswith("darwin"):
# TODO: Move these out to configuration
cmake_args += ["-DCMAKE_CXX_COMPILER=/usr/local/opt/llvm@16/bin/clang++"]
cmake_args += ["-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0"]

# Cross-compile support for macOS - respect ARCHFLAGS if set
archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", ""))
if archs:
cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))]

# Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level
# across all generators.
if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
# self.parallel is a Python 3 only way to set parallel jobs by hand
# using -j in the build_ext call, not supported by pip or PyPA-build.
if hasattr(self, "parallel") and self.parallel:
# CMake 3.12+ only.
build_args += [f"-j{self.parallel}"]

build_temp = Path(self.build_temp) / ext.name
if not build_temp.exists():
build_temp.mkdir(parents=True)

subprocess.run(
["cmake", ext.sourcedir, *cmake_args], cwd=build_temp, check=True
)
subprocess.run(["ninja", "update_bindings"], cwd=build_temp, check=True)
subprocess.run(
["cmake", "--build", ".", *build_args], cwd=build_temp, check=True
)

# Shared library files like (for ex.) _util.so must reside as (for ex.)
# ompl/util/_util.so or else they are placed incorrectly in the final
# wheel.
for f in ["base", "control", "geometric", "tools", "util"]:
subprocess.run(
[f"cp {extdir}/_{f}.so {extdir}/ompl/{f}/"],
cwd=build_temp,
check=True,
shell=True,
)


setup(
name='ompl',
version='1.6.0',
description='The Open Motion Planning Library',
author='Ioan A. Șucan, Mark Moll, Zachary Kingston, Lydia E. Kavraki',
author_email='zak@rice.edu',
url='https://ompl.kavrakilab.org',
packages=['ompl'],
name="ompl",
version="1.6.0",
description="The Open Motion Planning Library",
author="Ioan A. Șucan, Mark Moll, Zachary Kingston, Lydia E. Kavraki",
author_email="zak@rice.edu",
url="https://ompl.kavrakilab.org",
ext_modules=[CMakeExtension("ompl", sourcedir="..")],
cmdclass={"build_ext": CMakeBuild},
packages=[
"ompl",
"ompl.base",
"ompl.control",
"ompl.geometric",
"ompl.tools",
"ompl.util",
],
package_dir={"ompl": "./ompl"},
)

0 comments on commit df058b7

Please sign in to comment.