diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml new file mode 100644 index 000000000..4c9320cf6 --- /dev/null +++ b/.github/workflows/build_wheels.yml @@ -0,0 +1,50 @@ +name: Build wheels + +on: [push, pull_request] + +jobs: + unix: + name: ${{ matrix.os }} | ${{ matrix.arch }} | ${{ matrix.python }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + # macos-13 is an intel runner, macos-14 is apple silicon + os: [ubuntu-latest, macos-13, macos-14] + arch: [auto, aarch64] + python: [cp37-*, cp38-*, cp39-*, cp310-*, cp311-*, cp312-*, + pp37-*, pp38-*, pp39-*, pp310-*] + include: + - os: macos-13 + macos_deployment_target: 13 + - os: macos-14 + macos_deployment_target: 14 + exclude: + - os: macos-13 + arch: aarch64 + - os: macos-14 + arch: aarch64 + - os: macos-14 + python: cp37-* + - os: macos-14 + python: pp37-* + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Build wheels + uses: pypa/cibuildwheel@v2.19.2 + env: + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos_deployment_target }} + CIBW_ARCHS: ${{ matrix.arch }} + CIBW_BUILD: ${{ matrix.python }} + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl diff --git a/.gitignore b/.gitignore index 07ca244b8..64507593f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ docs/_build/ .idea/ .settings/ .project -.classpath \ No newline at end of file +.classpath +.venv +.DS_Store \ No newline at end of file diff --git a/docs/content/build-instructions.md b/docs/content/build-instructions.md index 13cb60a85..7e976e35b 100644 --- a/docs/content/build-instructions.md +++ b/docs/content/build-instructions.md @@ -21,6 +21,14 @@ release or the latest `master`, we recommend referring to the copy of this document (`docs/content/build-instructions.md`) found in your source distribution. +## Installing the Python module + +To build the Python module from source and install it, run: + +``` +pip3 install -v . +``` + ## CMake Overview These instructions assume that you will build in a directory named `build` as diff --git a/lcm-logger/CMakeLists.txt b/lcm-logger/CMakeLists.txt index e1f2f5a66..51e90c5e2 100644 --- a/lcm-logger/CMakeLists.txt +++ b/lcm-logger/CMakeLists.txt @@ -1,8 +1,8 @@ add_executable(lcm-logger lcm_logger.c glib_util.c) -target_link_libraries(lcm-logger lcm GLib2::glib) +target_link_libraries(lcm-logger lcm-static GLib2::glib) add_executable(lcm-logplayer lcm_logplayer.c) -target_link_libraries(lcm-logplayer lcm GLib2::glib) +target_link_libraries(lcm-logplayer lcm-static GLib2::glib) install(TARGETS lcm-logger diff --git a/lcm-python/CMakeLists.txt b/lcm-python/CMakeLists.txt index 77a3b0d12..d65bfaf53 100644 --- a/lcm-python/CMakeLists.txt +++ b/lcm-python/CMakeLists.txt @@ -1,13 +1,22 @@ # FindPython added in 3.12 cmake_minimum_required(VERSION 3.12) -execute_process( - COMMAND "${Python_EXECUTABLE}" -c "if True: - from sysconfig import get_path - from os.path import sep - print(get_path('platlib').replace(get_path('data') + sep, ''))" - OUTPUT_VARIABLE PYTHON_SITE - OUTPUT_STRIP_TRAILING_WHITESPACE) +option(LCM_PIP_BUILD "When true, adjusts install path to something appropriate for a Python wheel" OFF) + +if (NOT LCM_PIP_BUILD) + execute_process( + COMMAND "${Python_EXECUTABLE}" -c "if True: + from sysconfig import get_path + from os.path import sep + print(get_path('platlib').replace(get_path('data') + sep, ''))" + OUTPUT_VARIABLE PYTHON_SITE + OUTPUT_STRIP_TRAILING_WHITESPACE) + + set(PYTHON_INSTALL_DIR ${PYTHON_SITE}/lcm) +else() + set(PYTHON_INSTALL_DIR lcm) +endif() + set(lcm_python_sources module.c @@ -28,11 +37,19 @@ if (WIN32 AND NOT CYGWIN) set_target_properties(lcm-python PROPERTIES SUFFIX .pyd) endif () -find_package (Python COMPONENTS Interpreter Development) +if (WIN32) + find_package(Python COMPONENTS Interpreter Development.Module) -target_include_directories(lcm-python PRIVATE - ${Python_INCLUDE_DIRS} -) + target_include_directories(lcm-python PRIVATE + ${Python_INCLUDE_DIRS} + ) +else () + find_package(Python3 COMPONENTS Interpreter Development.Module) + + target_include_directories(lcm-python PRIVATE + ${Python3_INCLUDE_DIRS} + ) +endif () target_link_libraries(lcm-python PRIVATE lcm-static @@ -47,8 +64,8 @@ else() endif() install(TARGETS lcm-python - RUNTIME DESTINATION ${PYTHON_SITE}/lcm - LIBRARY DESTINATION ${PYTHON_SITE}/lcm + RUNTIME DESTINATION ${PYTHON_INSTALL_DIR} + LIBRARY DESTINATION ${PYTHON_INSTALL_DIR} ) lcm_copy_file_target(lcm-python-init @@ -56,4 +73,4 @@ lcm_copy_file_target(lcm-python-init ${CMAKE_BINARY_DIR}/python/lcm/__init__.py ) -install(FILES lcm/__init__.py DESTINATION ${PYTHON_SITE}/lcm) +install(FILES lcm/__init__.py DESTINATION ${PYTHON_INSTALL_DIR}) diff --git a/lcm-python/lcm/__init__.py b/lcm-python/lcm/__init__.py index e3e78caf9..5f83afe52 100644 --- a/lcm-python/lcm/__init__.py +++ b/lcm-python/lcm/__init__.py @@ -1,5 +1,6 @@ import os import sys +import subprocess # Attempt to be backwards compatible if sys.version_info >= (3, 6): @@ -164,3 +165,38 @@ def tell (self): @rtype: int """ return self.c_eventlog.ftell () + +LCM_BIN_DIR = os.path.join(os.path.dirname(__file__), '..', 'bin') + +def run_script(name: str, args) -> int: + return subprocess.call([os.path.join(LCM_BIN_DIR, name), *args], close_fds=False) + +def run_example(): + raise SystemExit(run_script('lcm-example', sys.argv[1:])) + +def run_gen(): + raise SystemExit(run_script('lcm-gen', sys.argv[1:])) + +def run_logfilter(): + raise SystemExit(run_script('lcm-logfilter', sys.argv[1:])) + +def run_logger(): + raise SystemExit(run_script('lcm-logger', sys.argv[1:])) + +def run_logplayer(): + raise SystemExit(run_script('lcm-logplayer', sys.argv[1:])) + +def run_logplayer_gui(): + raise SystemExit(run_script('lcm-logplayer-gui', sys.argv[1:])) + +def run_sink(): + raise SystemExit(run_script('lcm-sink', sys.argv[1:])) + +def run_source(): + raise SystemExit(run_script('lcm-source', sys.argv[1:])) + +def run_spy(): + raise SystemExit(run_script('lcm-spy', sys.argv[1:])) + +def run_tester(): + raise SystemExit(run_script('lcm-tester', sys.argv[1:])) diff --git a/lcm-python/setup.py b/lcm-python/setup.py deleted file mode 100644 index e015d2075..000000000 --- a/lcm-python/setup.py +++ /dev/null @@ -1,110 +0,0 @@ -import subprocess -from distutils.core import setup, Extension -from distutils import msvccompiler -import os -import sys - -sources = [ \ - "module.c", - "pyeventlog.c", - "pylcm.c", - "pylcm_subscription.c", - os.path.join("..", "lcm", "eventlog.c"), - os.path.join("..", "lcm", "lcm.c"), - os.path.join("..", "lcm", "lcm_file.c"), - os.path.join("..", "lcm", "lcm_memq.c"), - os.path.join("..", "lcm", "lcm_mpudpm.c"), - os.path.join("..", "lcm", "lcm_tcpq.c"), - os.path.join("..", "lcm", "lcmtypes", "channel_port_map_update_t.c"), - os.path.join("..", "lcm", "lcmtypes", "channel_to_port_t.c"), - os.path.join("..", "lcm", "lcm_udpm.c"), - os.path.join("..", "lcm", "ringbuffer.c"), - os.path.join("..", "lcm", "udpm_util.c") - ] - - -lcm_version_info = {} -with open(os.path.join("..", "lcm", "lcm_version.h"), 'r') as lcm_version_file: - for line in lcm_version_file: - if line.startswith('#define LCM_VERSION'): - parts = line.strip().split() - lcm_version_info[parts[1]] = parts[2] - -lcm_version = \ - '%(LCM_VERSION_MAJOR)s.%(LCM_VERSION_MINOR)s.%(LCM_VERSION_PATCH)s' \ - % lcm_version_info - -include_dirs = [".."] -define_macros = [('LCM_PYTHON','')] -library_dirs = [] -libraries = [] -extra_compile_args = [] - -if os.name == 'nt': - # check for GLIB_PATH environment var, exit with error if not found - glibPath = os.getenv('GLIB_PATH') - if not glibPath: - sys.exit('GLIB_PATH environment variable not set.') - - include_dirs += [ \ - os.path.join("..", "WinSpecific\include"), - os.path.join("..", "WinSpecific"), - os.path.join(glibPath, "include", "glib-2.0"), - os.path.join(glibPath, "lib", "glib-2.0", "include") ] - library_dirs.append(os.path.join(glibPath, 'lib')) - - # define additional macro WIN32, used to discriminate win specific code - define_macros += [('WIN32', 1)] - - libraries = [ 'Ws2_32', 'glib-2.0' ] - - # compiler arguments - # /TP enforces compilation of code as c++ - extra_compile_args = [ '/TP' ] - - # we need to patch the msvccompiler.MSVCCompiler class to compile - # .c C files as C++ code (/TP switch for MSVC) - # the default behaviour generates the command line switch /Tc for - # every .c source file - msvccompiler.MSVCCompiler._c_extensions = [] - msvccompiler.MSVCCompiler._cpp_extensions.append('.c') - - sources.append(os.path.join("..", "lcm", "windows", "WinPorting.cpp")) - -else: - pkg_deps = "glib-2.0" - - # detect terminal encoding. If the encoding is not detected, default to UTF-8 - encoding = sys.stdout.encoding or 'UTF-8' - - # include path - pkgconfig_include_flags = subprocess.check_output( ["pkg-config", "--cflags-only-I", pkg_deps] ).decode(encoding) - include_dirs += [ t[2:] for t in pkgconfig_include_flags.split() ] - - # libraries - pkgconfig_lflags = subprocess.check_output( ["pkg-config", "--libs-only-l", pkg_deps] ).decode(encoding) - libraries = [ t[2:] for t in pkgconfig_lflags.split() ] - - # link directories - pkgconfig_biglflags = subprocess.check_output( ["pkg-config", "--libs-only-L", pkg_deps ] ).decode(encoding) - library_dirs = [ t[2:] for t in pkgconfig_biglflags.split() ] - - # other compiler flags - pkgconfig_cflags = subprocess.check_output( ["pkg-config", "--cflags", pkg_deps] ).decode(encoding).split() - extra_compile_args = [ \ - '-Wno-strict-prototypes', - "-D_FILE_OFFSET_BITS=64", - "-D_LARGEFILE_SOURCE", - "-std=gnu99" ] + pkgconfig_cflags - -pylcm_extension = Extension("lcm._lcm", - sources, - include_dirs=include_dirs, - define_macros=define_macros, - library_dirs=library_dirs, - libraries=libraries, - extra_compile_args=extra_compile_args) - -setup(name="lcm", version=lcm_version, - ext_modules=[pylcm_extension], - packages=["lcm"]) diff --git a/liblcm-test/CMakeLists.txt b/liblcm-test/CMakeLists.txt index 3e28be162..5e05c481c 100644 --- a/liblcm-test/CMakeLists.txt +++ b/liblcm-test/CMakeLists.txt @@ -1,29 +1,29 @@ add_executable(lcm-sink lcm-sink.c) -target_link_libraries(lcm-sink lcm) +target_link_libraries(lcm-sink lcm-static) add_executable(lcm-source lcm-source.c) -target_link_libraries(lcm-source lcm) +target_link_libraries(lcm-source lcm-static) add_executable(lcm-tester lcm-tester.c) -target_link_libraries(lcm-tester lcm GLib2::glib) +target_link_libraries(lcm-tester lcm-static GLib2::glib) if(WIN32) target_link_libraries(lcm-tester wsock32 ws2_32) endif() add_executable(lcm-example lcm-example.c) -target_link_libraries(lcm-example lcm) +target_link_libraries(lcm-example lcm-static) if(WIN32) target_link_libraries(lcm-example ws2_32) endif() add_executable(lcm-logfilter lcm-logfilter.c) -target_link_libraries(lcm-logfilter lcm GLib2::glib) +target_link_libraries(lcm-logfilter lcm-static GLib2::glib) add_executable(lcm-buftest-receiver buftest-receiver.c) -target_link_libraries(lcm-buftest-receiver lcm GLib2::glib) +target_link_libraries(lcm-buftest-receiver lcm-static GLib2::glib) add_executable(lcm-buftest-sender buftest-sender.c) -target_link_libraries(lcm-buftest-sender lcm GLib2::glib) +target_link_libraries(lcm-buftest-sender lcm-static GLib2::glib) install(TARGETS lcm-sink diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..d3912881b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "lcm" +version = "1.5.0" +description = "Lightweight Communication and Marshalling" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + 'Development Status :: 4 - Beta', + 'Topic :: Communications', + 'Framework :: Robot Framework', +] + +[project.scripts] +lcm-example = "lcm:run_example" +lcm-gen = "lcm:run_gen" +lcm-logfilter = "lcm:run_logfilter" +lcm-logger = "lcm:run_logger" +lcm-logplayer = "lcm:run_logplayer" +lcm-logplayer-gui = "lcm:run_logplayer_gui" +lcm-sink = "lcm:run_sink" +lcm-source = "lcm:run_source" +lcm-spy = "lcm:run_spy" +lcm-tester = "lcm:run_tester" + +[project.optional-dependencies] +test = ["pytest"] + +[tool.scikit-build] +cmake.args = ["-DLCM_INSTALL_PKGCONFIG=OFF", "-DLCM_PIP_BUILD=ON"] +wheel.expand-macos-universal-tags = true +build.tool-args = ["-j4"] + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] +xfail_strict = true +log_cli_level = "INFO" +filterwarnings = ["error"] + +[tool.cibuildwheel] +build-verbosity = 1 + +[tool.cibuildwheel.linux] +before-all = "yum install java-1.8.0-openjdk-devel -y" + +[[tool.cibuildwheel.overrides]] +select = "*-musllinux*" +before-all = ""