diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index f3a30045ce..d23e64f549 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -38,8 +38,13 @@ jobs: python-version: '3.11' - name: Install numpy - # Need numpy to use SWIG numpy typemaps. - run: python3 -m pip install numpy==2.0 + #Need numpy to use SWIG numpy typemaps. + run: | + python3 -m pip install numpy==2.0 + python3 -m pip install wheel + python3 -m pip install build + pip install --upgrade pip setuptools wheel + pip install delvewheel - name: Install SWIG run: | @@ -68,7 +73,7 @@ jobs: # /W0 disables warnings. # https://msdn.microsoft.com/en-us/library/19z1t1wy.aspx # TODO: CMake provides /W3, which overrides our /W0 - cmake -E env CXXFLAGS="/W0 /MD" cmake $env:GITHUB_WORKSPACE/dependencies -LAH -G"Visual Studio 17 2022" -A x64 -DCMAKE_INSTALL_PREFIX=~/opensim_dependencies_install -DSUPERBUILD_ezc3d=ON -DOPENSIM_WITH_CASADI=ON + cmake -E env CXXFLAGS="/W0 /MD" cmake $env:GITHUB_WORKSPACE/dependencies -LAH -G"Visual Studio 17 2022" -A x64 -DCMAKE_INSTALL_PREFIX=~/opensim_dependencies_install -DSUPERBUILD_ezc3d=ON -DOPENSIM_WITH_CASADI=ON -DOPENSIM_PYTHON_STANDALONE=ON cmake --build . --config Release -- /maxcpucount:4 - name: Configure opensim-core @@ -79,7 +84,7 @@ jobs: # TODO: Can remove /WX when we use that in CMakeLists.txt. # Set the CXXFLAGS environment variable to turn warnings into errors. # Skip timing test section included by default. - cmake -E env CXXFLAGS="/WX /MD -DSKIP_TIMING" cmake $env:GITHUB_WORKSPACE -LAH -G"Visual Studio 17 2022" -A x64 -DCMAKE_INSTALL_PREFIX=~/opensim-core-install -DOPENSIM_DEPENDENCIES_DIR=~/opensim_dependencies_install -DOPENSIM_C3D_PARSER=ezc3d -DOPENSIM_WITH_CASADI=on -DBUILD_PYTHON_WRAPPING=on -DBUILD_JAVA_WRAPPING=on -DPython3_ROOT_DIR=C:\hostedtoolcache\windows\Python\3.11.9\x64 + cmake -E env CXXFLAGS="/WX /MD -DSKIP_TIMING" cmake $env:GITHUB_WORKSPACE -LAH -G"Visual Studio 17 2022" -A x64 -DCMAKE_INSTALL_PREFIX=~/opensim-core-install -DOPENSIM_DEPENDENCIES_DIR=~/opensim_dependencies_install -DOPENSIM_C3D_PARSER=ezc3d -DOPENSIM_WITH_CASADI=on -DBUILD_PYTHON_WRAPPING=on -DBUILD_JAVA_WRAPPING=on -DBUILD_PYTHON_WHEELS=on -DPython3_ROOT_DIR=C:\hostedtoolcache\windows\Python\3.11.9\x64 $env:match = cmake -L . | Select-String -Pattern OPENSIM_QUALIFIED_VERSION $version = $env:match.split('=')[1] echo $version @@ -106,13 +111,22 @@ jobs: Copy-Item -Path "~/opensim-core-install" -Destination "opensim-core-${{ steps.configure.outputs.version }}" -Recurse 7z a "opensim-core-${{ steps.configure.outputs.version }}.zip" "opensim-core-${{ steps.configure.outputs.version }}" - # - name: Test Python bindings - # run: | - # echo "PYTHONPATH= $env:USERPROFILE/opensim-core-install/bin">> $GITHUB_ENV - # # Move to the installed location of the python package. - # cd ~/opensim-core-install/sdk/python - # # Run python tests. - # python -m unittest discover --start-directory opensim/tests --verbose +# - name: Build wheel +# run: | +# cd ~/opensim-core-install/sdk/python +# python3 setup.py bdist_wheel +# - name: Upload windows wheel +# uses: actions/upload-artifact@v4 +# with: +# name: opensim-4.5.2-cp311-cp311-win_amd64.whl +# path: ~/opensim-core-install/sdk/python/dist/opensim-4.5.2-cp311-cp311-win_amd64.whl +# +# - name: Test Python bindings +# run: | +# cd ~/opensim-core-install/sdk/python +# pip install dist/opensim-4.5.2-cp311-cp311-win_amd64.whl +# # Run the python tests, verbosely. +# python3 -m unittest discover --start-directory opensim/tests --verbose - name: Upload opensim-core uses: actions/upload-artifact@v4 @@ -267,6 +281,8 @@ jobs: brew install pkgconfig autoconf libtool automake wget pcre doxygen llvm brew reinstall gcc pip3 install numpy==2.0 + pip3 install wheel + pip3 install build gfortran -v mkdir gfortran_version gfortran -v &> gfortran_version/gfortran_version.txt @@ -309,6 +325,7 @@ jobs: DEP_CMAKE_ARGS+=(-DOPENSIM_WITH_CASADI=ON) DEP_CMAKE_ARGS+=(-DOPENSIM_DISABLE_LOG_FILE=ON) DEP_CMAKE_ARGS+=(-DCMAKE_OSX_DEPLOYMENT_TARGET=11) + printf '%s\n' "${DEP_CMAKE_ARGS[@]}" cmake "${DEP_CMAKE_ARGS[@]}" make --jobs 4 @@ -325,6 +342,8 @@ jobs: OSIM_CMAKE_ARGS+=(-DCMAKE_OSX_DEPLOYMENT_TARGET=11) OSIM_CMAKE_ARGS+=(-DOPENSIM_C3D_PARSER=ezc3d) OSIM_CMAKE_ARGS+=(-DBUILD_PYTHON_WRAPPING=on -DBUILD_JAVA_WRAPPING=on) + OSIM_CMAKE_ARGS+=(-DBUILD_PYTHON_WHEELS=on) + OSIM_CMAKE_ARGS+=(-DOPENSIM_WITH_CASADI=ON) OSIM_CMAKE_ARGS+=(-DSWIG_EXECUTABLE=$HOME/swig/bin/swig) OSIM_CMAKE_ARGS+=(-DOPENSIM_INSTALL_UNIX_FHS=OFF) OSIM_CMAKE_ARGS+=(-DOPENSIM_DOXYGEN_USE_MATHJAX=off) @@ -358,9 +377,21 @@ jobs: zip --symlinks --recurse-paths --quiet opensim-core-${{ steps.configure.outputs.version }}.zip opensim-core-${{ steps.configure.outputs.version }} mv opensim-core-${{ steps.configure.outputs.version }} $GITHUB_WORKSPACE/../opensim-core-install + - name: Build wheel + run: | + cd $GITHUB_WORKSPACE/../opensim-core-install/sdk/Python + python3 -m build --wheel + + - name: Upload osx wheel + uses: actions/upload-artifact@v4 + with: + name: opensim-4.5.2-cp311-cp311-macosx_15_0_universal2.whl + path: /Users/runner/work/opensim-core/opensim-core-install/sdk/python/dist/opensim-4.5.2-cp311-cp311-macosx_15_0_universal2.whl + - name: Test Python bindings run: | cd $GITHUB_WORKSPACE/../opensim-core-install/sdk/Python + pip3 install /Users/runner/work/opensim-core/opensim-core-install/sdk/python/dist/opensim-4.5.2-cp311-cp311-macosx_15_0_universal2.whl # Run the python tests, verbosely. python3 -m unittest discover --start-directory opensim/tests --verbose @@ -508,6 +539,8 @@ jobs: run: | pip install pip --upgrade pip install numpy==2.0 + pip install build + pip install wheel - name: Install packages run: sudo apt-get update && sudo apt-get install --yes build-essential libtool autoconf pkg-config gfortran libopenblas-dev liblapack-dev freeglut3-dev libxi-dev libxmu-dev doxygen patchelf @@ -561,6 +594,8 @@ jobs: OSIM_CMAKE_ARGS+=(-DOPENSIM_DEPENDENCIES_DIR=~/opensim_dependencies_install) OSIM_CMAKE_ARGS+=(-DOPENSIM_C3D_PARSER=ezc3d) OSIM_CMAKE_ARGS+=(-DBUILD_PYTHON_WRAPPING=on -DBUILD_JAVA_WRAPPING=on) + OSIM_CMAKE_ARGS+=(-DBUILD_PYTHON_WHEELS=on) + OSIM_CMAKE_ARGS+=(-DOPENSIM_WITH_CASADI=ON) OSIM_CMAKE_ARGS+=(-DSWIG_DIR=~/swig/share/swig) OSIM_CMAKE_ARGS+=(-DSWIG_EXECUTABLE=~/swig/bin/swig) OSIM_CMAKE_ARGS+=(-DOPENSIM_INSTALL_UNIX_FHS=OFF) @@ -597,11 +632,23 @@ jobs: zip --symlinks --recurse-paths --quiet opensim-core-${{ steps.configure.outputs.version }}-ubuntu22.zip opensim-core-${{ steps.configure.outputs.version }} mv opensim-core-${{ steps.configure.outputs.version }} $GITHUB_WORKSPACE/../opensim-core-install + - name: Build wheel + run: | + cd $GITHUB_WORKSPACE/../opensim-core-install/sdk/Python + python3 -m build --wheel + + - name: Upload linux wheel + uses: actions/upload-artifact@v4 + with: + name: opensim-4.5.2-cp311-cp311-linux_x86_64.whl + path: /home/runner/work/opensim-core/opensim-core-install/sdk/Python/dist/opensim-4.5.2-cp311-cp311-linux_x86_64.whl + - name: Test Python bindings run: | - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/opensim_dependencies_install/simbody/lib + export LD_LIBRARY_PATH="" cd $GITHUB_WORKSPACE/../opensim-core-install/sdk/Python - # Run the python tests, verbosely. + pip install /home/runner/work/opensim-core/opensim-core-install/sdk/Python/dist/opensim-4.5.2-cp311-cp311-linux_x86_64.whl + # Run the python tests, verbosely python3 -m unittest discover --start-directory opensim/tests --verbose - name: Upload opensim-core diff --git a/Bindings/Python/CMakeLists.txt b/Bindings/Python/CMakeLists.txt index e8784e93d3..34b9d4e294 100644 --- a/Bindings/Python/CMakeLists.txt +++ b/Bindings/Python/CMakeLists.txt @@ -251,7 +251,6 @@ OpenSimPutFileInPythonPackage("${CMAKE_CURRENT_BINARY_DIR}/version.py" opensim) # Copy setup.py for each build configuration. OpenSimPutFileInPythonPackage("${CMAKE_CURRENT_SOURCE_DIR}/setup.py" ".") - # __init__.py. OpenSimPutFileInPythonPackage("${CMAKE_CURRENT_SOURCE_DIR}/__init__.py" opensim) diff --git a/Bindings/Python/examples/build_simple_arm_model.py b/Bindings/Python/examples/build_simple_arm_model.py index 00cc676f3a..1f914ad6ff 100644 --- a/Bindings/Python/examples/build_simple_arm_model.py +++ b/Bindings/Python/examples/build_simple_arm_model.py @@ -39,7 +39,7 @@ # Define global model where the arm lives. arm = osim.Model() -if not running_as_test: arm.setUseVisualizer(True) +#if not running_as_test: arm.setUseVisualizer(True) # --------------------------------------------------------------------------- # Create two links, each with a mass of 1 kg, centre of mass at the body's diff --git a/Bindings/Python/setup.py b/Bindings/Python/setup.py index dbfeafae6a..c2b11ee580 100644 --- a/Bindings/Python/setup.py +++ b/Bindings/Python/setup.py @@ -3,6 +3,7 @@ import os import sys from setuptools import setup +from setuptools.dist import Distribution # This provides a list of relative paths to all the dependencies in the bin folder. # Only when installed locally via "python -m pip install ." in Windows @@ -24,6 +25,10 @@ execfile('opensim/version.py') else: exec(compile(open('opensim/version.py').read(), 'opensim/version.py', 'exec')) +class BinaryDistribution(Distribution): + """Distribution which always forces a binary package with platform name""" + def has_ext_modules(foo): + return True setup(name='opensim', version=__version__, @@ -35,8 +40,7 @@ packages=['opensim'], # Copy the bin_files and geometry_files into the opensim package directory data_files=[ - ('Lib/site-packages/opensim', bin_files), - ('Lib/site-packages/opensim/Geometry', geometry_files) + ('opensim', bin_files) ], # The last 3 entries are for if OPENSIM_PYTHON_STANDALONE is ON. # The asterisk after the extension is to handle version numbers on Linux. @@ -51,8 +55,13 @@ classifiers=[ 'Intended Audience :: Science/Research', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Topic :: Scientific/Engineering :: Physics', ], + install_requires=[ + "numpy>=2.0" + ], + distclass=BinaryDistribution ) +# To build the wheel, run the following command: +#python setup.py bdist_wheel diff --git a/CHANGELOG.md b/CHANGELOG.md index 1533745333..37a5abc4af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ performance and stability in wrapping solutions. - Updated required language level to C++20. (#3929) - Breaking: removed the `operator==` and `operator<` overloads in `ControlLinearNode` and replaced usages with the equivalent utility functions `isEqual()` and `isLessThan()`. (#4095) - Made various changes to support builds on Ubuntu 24.04 with GCC 13. (#4186) +- Support building PYPI distribution python wheels on all platforms, upgrade builds to python 3.11 and numpy 2.0. (#4189) v4.5.2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 27080bc214..8767bac2e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -500,6 +500,12 @@ set(BUILD_JAVA_WRAPPING OFF CACHE BOOL "Build Java wrapping (needed if you're bu set(BUILD_PYTHON_WRAPPING OFF CACHE BOOL "Build Python wrapping (needed if you're building the Python wrapping; requires that you have SWIG and Python installed on your machine.)") +set(BUILD_PYTHON_WHEELS OFF CACHE BOOL "Build Python wrapping into a pypi package wheel") + +if(BUILD_PYTHON_WHEELS) + set(OPENSIM_PYTHON_STANDALONE ON CACHE BOOL "Enable Standalone python packaging" FORCE) +endif() + set(OPENSIM_PYTHON_VERSION 3) if(${BUILD_JAVA_WRAPPING}) @@ -564,31 +570,6 @@ if(${BUILD_PYTHON_WRAPPING}) # OPENSIM_INSTALL_PYTHONDIR is used in OpenSimAddLibrary (in # OpenSimMacros.cmake). - if(APPLE AND ${OPENSIM_PYTHON_VERSION} STREQUAL "2") - # If you have Homebrew's Python2, then by default, PythonInterp finds - # Apple's Python, but PythonLibs finds Homebrew's Python, causing - # runtime crashes. This also occurs if one has Anaconda Python. - # So we use the python-config executable to get the - # correct library and include directory. - # https://github.com/Homebrew/legacy-homebrew/issues/25118 - execute_process(COMMAND "${Python3_EXECUTABLE}-config" --prefix - OUTPUT_VARIABLE python_prefix - OUTPUT_STRIP_TRAILING_WHITESPACE) - string(CONCAT pyinc_desc - "Location of Python header files, to compile bindings. " - "Must be consistent with PYTHON_EXECUTABLE.") - set(PYTHON_INCLUDE_DIR - "${python_prefix}/include/python${python_version_majmin}/" - CACHE PATH "${pyinc_desc}") - - string(CONCAT pylib_desc - "Location of Python library, to compile bindings. " - "Must be consistent with PYTHON_EXECUTABLE.") - set(PYTHON_LIBRARY - "${python_prefix}/lib/libpython${python_version_majmin}.dylib" - CACHE FILEPATH "${pylib_desc}") - endif() - # Most users need to copy dependencies if wrapping is on. Warn user # if it isn't on. if (NOT OPENSIM_COPY_DEPENDENCIES) @@ -830,7 +811,6 @@ endif() if (OPENSIM_WITH_CASADI) if(UNIX) pkg_check_modules(IPOPT REQUIRED ipopt IMPORTED_TARGET) - if(OPENSIM_COPY_DEPENDENCIES AND APPLE) # Extract actual name of the library libgfortran, gcc, quadmath to install along on OSX. # These show only as dependencies of libipopt, and libgcc is in same folder as libgfortran. @@ -847,7 +827,6 @@ if (OPENSIM_WITH_CASADI) "otool -L ${IPOPT_LIBDIR}/libipopt.dylib | grep 'libquadmath' | awk '{print $1}'" OUTPUT_VARIABLE libquadmath_name OUTPUT_STRIP_TRAILING_WHITESPACE) - install(FILES ${IPOPT_LIBDIR}/libipopt.3.dylib ${IPOPT_LIBDIR}/libipopt.dylib @@ -858,7 +837,6 @@ if (OPENSIM_WITH_CASADI) ${libquadmath_name} ${libgcc_name} DESTINATION ${CMAKE_INSTALL_LIBDIR}) - elseif(OPENSIM_COPY_DEPENDENCIES) get_filename_component(gcc_libdir "${pkgcfg_lib_IPOPT_gfortran}" DIRECTORY) @@ -877,6 +855,14 @@ if (OPENSIM_WITH_CASADI) ${quadmath} DESTINATION ${CMAKE_INSTALL_LIBDIR}) endif() + if(BUILD_PYTHON_WRAPPING AND OPENSIM_PYTHON_STANDALONE) + OpenSimInstallDependencyLibraries(ipopt "${IPOPT_LIBDIR}" + "${IPOPT_LIBDIR}" "${OPENSIM_INSTALL_PYTHONDIR}/opensim") + OpenSimInstallDependencyLibraries(coin "${IPOPT_LIBDIR}" + "${IPOPT_LIBDIR}" "${OPENSIM_INSTALL_PYTHONDIR}/opensim") + OpenSimInstallDependencyLibraries(metis "${IPOPT_LIBDIR}" + "${IPOPT_LIBDIR}" "${OPENSIM_INSTALL_PYTHONDIR}/opensim") + endif() else() set(Ipopt_ROOT_DIR ${OPENSIM_DEPENDENCIES_DIR}/ipopt) message("Ipopt_ROOT_DIR ${Ipopt_ROOT_DIR}") @@ -1061,3 +1047,8 @@ add_subdirectory(doc) # Print a list of the dependencies that were found, and the features the user # chose. feature_summary(WHAT ALL) + +if (BUILD_PYTHON_WHEELS) +install(CODE "set(OPENSIM_INSTALL_PYTHONDIR \"${OPENSIM_INSTALL_PYTHONDIR}\")") +install (SCRIPT "${CMAKE_SOURCE_DIR}/buildWheel.cmake") +endif() diff --git a/OpenSim/Moco/CMakeLists.txt b/OpenSim/Moco/CMakeLists.txt index 5d81740044..1058b590e6 100644 --- a/OpenSim/Moco/CMakeLists.txt +++ b/OpenSim/Moco/CMakeLists.txt @@ -1,6 +1,5 @@ -# TODO: Use OpenSimAddLibrary() macro. set(MOCO_SOURCES osimMoco.h osimMocoDLL.h @@ -138,8 +137,13 @@ if(OPENSIM_WITH_CASADI) ) endif() -add_library(osimMoco SHARED ${MOCO_SOURCES}) -target_link_libraries(osimMoco PUBLIC osimTools) +OpenSimAddLibrary( + KIT Moco + AUTHORS "Chris_Dembia-Nick_Bianco" + LINKLIBS PUBLIC osimTools + SOURCES ${MOCO_SOURCES} + ) + if (OPENSIM_WITH_CASADI) target_link_libraries(osimMoco PRIVATE casadi) endif() @@ -148,11 +152,6 @@ target_include_directories(osimMoco INTERFACE $ $) -set_target_properties(osimMoco PROPERTIES - DEFINE_SYMBOL OSIMMOCO_EXPORTS - PROJECT_LABEL "osimMoco" - FOLDER "Libraries" - ) if (OPENSIM_WITH_CASADI) target_compile_definitions(osimMoco PUBLIC OPENSIM_WITH_CASADI) endif () @@ -166,7 +165,7 @@ target_compile_definitions(osimMoco PRIVATE OpenSimAddInstallRPATHSelf(TARGET osimMoco LOADER) OpenSimAddInstallRPATHSimbody(TARGET osimMoco LOADER FROM "${CMAKE_INSTALL_LIBDIR}") -install(TARGETS osimMoco EXPORT OpenSimTargets +install(TARGETS EXPORT OpenSimTargets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/buildWheel.cmake b/buildWheel.cmake new file mode 100644 index 0000000000..1964d09426 --- /dev/null +++ b/buildWheel.cmake @@ -0,0 +1,27 @@ +if (WIN32) + file(GLOB OPENSIM_ALL_LIBS "${CMAKE_INSTALL_PREFIX}/bin/*.dll") + set(DESTINATION_DIR ${CMAKE_INSTALL_PREFIX}/sdk/Python/opensim) + foreach(FILE ${OPENSIM_ALL_LIBS}) + file(COPY ${FILE} DESTINATION ${DESTINATION_DIR}) + endforeach() +elseif(APPLE) + file(GLOB OPENSIM_ALL_LIBS "${CMAKE_INSTALL_PREFIX}/lib/*.dylib" + "${CMAKE_INSTALL_PREFIX}/lib/*.a" + "${CMAKE_INSTALL_PREFIX}/sdk/lib/*.dylib" + "${CMAKE_INSTALL_PREFIX}/sdk/lib/*.a") + set(DESTINATION_DIR ${CMAKE_INSTALL_PREFIX}/${OPENSIM_INSTALL_PYTHONDIR}/opensim) + foreach(FILE ${OPENSIM_ALL_LIBS}) + file(COPY ${FILE} DESTINATION ${DESTINATION_DIR}) + endforeach() +else() + file(GLOB OPENSIM_ALL_LIBS "${CMAKE_INSTALL_PREFIX}/sdk/lib/*.so*" "${CMAKE_INSTALL_PREFIX}/lib/*.so*") + set(DESTINATION_DIR ${CMAKE_INSTALL_PREFIX}/${OPENSIM_INSTALL_PYTHONDIR}/opensim) + foreach(FILE ${OPENSIM_ALL_LIBS}) + file(COPY ${FILE} DESTINATION ${DESTINATION_DIR}) + endforeach() + # add $ORIGIN to rpath as layout has changed + file(GLOB SIMTK_LIBS "${CMAKE_INSTALL_PREFIX}/${OPENSIM_INSTALL_PYTHONDIR}/opensim/libSimTK*.so*") + foreach(slib ${SIMTK_LIBS}) + execute_process(COMMAND bash "-c" "patchelf --force-rpath --set-rpath '$ORIGIN' '${slib}'") + endforeach() +endif() diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index f3ffa97d4e..bf992e7979 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -46,7 +46,7 @@ install(FILES OpenSimMacros.cmake install(EXPORT OpenSimTargets DESTINATION "${OPENSIM_INSTALL_CMAKEDIR}") -if(UNIX AND OPENSIM_COPY_DEPENDENCIES AND (NOT (DEFINED OPENSIM_WITH_CASADI AND NOT ${OPENSIM_WITH_CASADI}))) +if(UNIX AND OPENSIM_COPY_DEPENDENCIES AND OPENSIM_WITH_CASADI) # Temporary hack to package dependencies on Macs. # TODO if we're building a standalone binary distribution, we should # use superbuild to build the dependencies.