diff --git a/.github/workflows/build_pymeos_cffi.yml b/.github/workflows/build_pymeos_cffi.yml new file mode 100644 index 00000000..6eaf8b2d --- /dev/null +++ b/.github/workflows/build_pymeos_cffi.yml @@ -0,0 +1,219 @@ +name: Build PyMEOS CFFI + +on: + create: + tags: + - "pymeos-cffi-[0-9]+.[0-9]+.[0-9]+*" + +jobs: + build_sdist: + name: Build PyMEOS CFFI source distribution + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: 3.8 + cache: "pip" + + - name: Setup pip + run: | + python -m pip install --upgrade pip + python -m pip install build + + - name: Build sdist + working-directory: pymeos_cffi + run: | + python -m build -s + ls -l dist + + - uses: actions/upload-artifact@v4 + with: + name: pymeos_cffi-sdist + path: ./pymeos_cffi/dist/pymeos_cffi-*.tar.gz + + build_wheels: + name: Build PyMEOS CFFI for ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, macos-13, macos-14 ] + include: + - ld_prefix: "/usr/local" + - os: macos-14 + ld_prefix: "/opt/homebrew" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Update brew + if: matrix.os == 'macos-13' + # Necessary to avoid issue with macOS runners. See + # https://github.com/actions/runner-images/issues/4020 + run: | + brew reinstall python@3.12 || brew link --overwrite python@3.12 + brew reinstall python@3.11 || brew link --overwrite python@3.11 + brew update + + - name: Get dependencies from homebrew (cache) + uses: tecolicom/actions-use-homebrew-tools@v1 + if: runner.os == 'macOS' + with: + tools: cmake libpq proj json-c gsl geos + + - name: Get PROJ version + id: proj_version + if: runner.os == 'macOS' + run: | + proj_version=$(brew list --versions proj) + proj_version=${proj_version#* } + echo "proj_version=$proj_version" >> $GITHUB_OUTPUT + + - name: Install MEOS + if: runner.os == 'macOS' + run: | + git clone --depth 1 https://github.com/MobilityDB/MobilityDB + mkdir MobilityDB/build + cd MobilityDB/build + cmake .. -DMEOS=on + make -j + sudo make install + + - name: Setup Python + uses: actions/setup-python@v5 + + - name: Install cibuildwheel + run: python -m pip install cibuildwheel==2.17.0 + + - name: Set PROJ_DATA (macOS) + if: runner.os == 'macOS' + run: | + PROJ_DATA=${{ matrix.ld_prefix }}/Cellar/proj/${{ steps.proj_version.outputs.proj_version }}/share/proj + echo "PROJ_DATA=$PROJ_DATA" >> $GITHUB_ENV + + - name: Set PROJ_DATA and JSON-C path (Linux) + if: runner.os == 'Linux' + run: | + PROJ_DATA=/usr/proj81/share/proj + echo "PROJ_DATA=$PROJ_DATA" >> $GITHUB_ENV + echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib64" >> $GITHUB_ENV + + - name: Build wheels + working-directory: pymeos_cffi + run: | + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib + export PACKAGE_DATA=1 + python -m cibuildwheel --output-dir wheelhouse + env: + # Disable PyPy builds on Linux since shapely has no built distributions for them + # Disable builds on musllinux + # Disable builds in linux architectures other than x86_64 + CIBW_SKIP: "pp*-manylinux* *musllinux*" + CIBW_ARCHS_LINUX: "x86_64" + CIBW_ENVIRONMENT_PASS_LINUX: PACKAGE_DATA LD_LIBRARY_PATH PROJ_DATA + CIBW_BEFORE_ALL_LINUX: > + yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm && + yum -y update && + yum -y install gcc gcc-c++ make cmake postgresql13-devel proj81-devel geos39-devel gsl-devel && + git clone --branch json-c-0.17 --depth 1 https://github.com/json-c/json-c && + mkdir json-c-build && + cd json-c-build && + cmake ../json-c && + make && + make install && + git clone --depth 1 https://github.com/MobilityDB/MobilityDB && + mkdir MobilityDB/build && + cd MobilityDB/build && + cmake .. -DMEOS=on -DGEOS_INCLUDE_DIR=/usr/geos39/include/ -DGEOS_LIBRARY=/usr/geos39/lib64/libgeos_c.so -DGEOS_CONFIG=/usr/geos39/bin/geos-config -DPROJ_INCLUDE_DIRS=/usr/proj81/include/ -DPROJ_LIBRARIES=/usr/proj81/lib/libproj.so && + make -j && + make install + + CIBW_TEST_COMMAND: "python -c \"from pymeos_cffi import meos_initialize, meos_finalize; meos_initialize('UTC'); meos_finalize()\"" + + - uses: actions/upload-artifact@v4 + with: + name: pymeos_cffi-wheels-${{ matrix.os }} + path: ./pymeos_cffi/wheelhouse/*.whl + + test_wheels: + name: Test PyMEOS CFFI wheel - Python ${{ matrix.python-version }} on ${{ matrix.os }} + needs: build_wheels + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] + os: [ ubuntu-latest, macos-13, macos-14 ] + exclude: + # Necessary due to issue with macOS runners. See + # https://github.com/actions/setup-python/issues/808 + # Can be removed once this PR is merged: + # https://github.com/actions/python-versions/pull/259 + - os: macos-14 + python-version: "3.8" + - os: macos-14 + python-version: "3.9" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download wheels + uses: actions/download-artifact@v4 + with: + name: pymeos_cffi-wheels-${{ matrix.os }} + path: ./pymeos_cffi_wheels + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install PyMEOS dependencies + run: | + python -m pip install --upgrade pip + pip install -f ./pymeos_cffi_wheels pymeos_cffi + pip install -r pymeos/dev-requirements.txt + + - name: Test PyMEOS with pytest + working-directory: pymeos + run: pytest + + upload_pypi: + name: Upload to PyPI + needs: [ test_wheels, build_sdist ] + runs-on: ubuntu-22.04 + steps: + - uses: actions/download-artifact@v4 + with: + path: ./dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + skip_existing: true + + create_release: + name: Create GitHub Release + needs: [ test_wheels, build_sdist ] + runs-on: ubuntu-22.04 + steps: + - uses: actions/download-artifact@v4 + with: + path: ./dist + merge-multiple: true + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: ./dist/* \ No newline at end of file diff --git a/pymeos_cffi/pymeos_cffi/builder/build_pymeos.py b/pymeos_cffi/pymeos_cffi/builder/build_pymeos.py index 0ffb0d4e..92a2bc35 100644 --- a/pymeos_cffi/pymeos_cffi/builder/build_pymeos.py +++ b/pymeos_cffi/pymeos_cffi/builder/build_pymeos.py @@ -1,5 +1,4 @@ -import platform -import sys +import os from cffi import FFI @@ -12,27 +11,13 @@ def get_library_dirs(): - if sys.platform == "linux": - return ["/usr/local/lib"] - elif sys.platform == "darwin": - if platform.processor() == "arm": - return ["/opt/homebrew/lib"] - else: - return ["/usr/local/lib"] - else: - raise NotImplementedError("Unsupported platform") + paths = ["/usr/local/lib", "/opt/homebrew/lib"] + return [path for path in paths if os.path.exists(path)] def get_include_dirs(): - if sys.platform == "linux": - return ["/usr/local/include"] - elif sys.platform == "darwin": - if platform.processor() == "arm": - return ["/opt/homebrew/include"] - else: - return ["/usr/local/include"] - else: - raise NotImplementedError("Unsupported platform") + paths = ["/usr/local/include", "/opt/homebrew/include"] + return [path for path in paths if os.path.exists(path)] ffibuilder.set_source( @@ -41,7 +26,7 @@ def get_include_dirs(): libraries=["meos"], library_dirs=get_library_dirs(), include_dirs=get_include_dirs(), -) # library name, for the linker +) if __name__ == "__main__": # not when running with setuptools ffibuilder.compile(verbose=True) diff --git a/pymeos_cffi/pymeos_cffi/builder/build_pymeos_functions_modifiers.py b/pymeos_cffi/pymeos_cffi/builder/build_pymeos_functions_modifiers.py index 2cf1093e..7dcd98b5 100644 --- a/pymeos_cffi/pymeos_cffi/builder/build_pymeos_functions_modifiers.py +++ b/pymeos_cffi/pymeos_cffi/builder/build_pymeos_functions_modifiers.py @@ -42,6 +42,14 @@ def textset_make_modifier(function: str) -> str: def meos_initialize_modifier(_: str) -> str: return """def meos_initialize(tz_str: "Optional[str]") -> None: + + if "PROJ_DATA" not in os.environ and "PROJ_LIB" not in os.environ: + proj_dir = os.path.join(os.path.dirname(__file__), "proj_data") + if os.path.exists(proj_dir): + # Assume we are in a wheel and the PROJ data is in the package + os.environ["PROJ_DATA"] = proj_dir + os.environ["PROJ_LIB"] = proj_dir + tz_str_converted = tz_str.encode('utf-8') if tz_str is not None else _ffi.NULL _lib.meos_initialize(tz_str_converted, _lib.py_error_handler)""" diff --git a/pymeos_cffi/pymeos_cffi/builder/templates/functions.py b/pymeos_cffi/pymeos_cffi/builder/templates/functions.py index 992dfd74..e79c4f88 100644 --- a/pymeos_cffi/pymeos_cffi/builder/templates/functions.py +++ b/pymeos_cffi/pymeos_cffi/builder/templates/functions.py @@ -1,4 +1,6 @@ import logging +import os + from datetime import datetime, timedelta, date from typing import Any, Tuple, Optional, List diff --git a/pymeos_cffi/pymeos_cffi/functions.py b/pymeos_cffi/pymeos_cffi/functions.py index c50e0ff5..4151409a 100644 --- a/pymeos_cffi/pymeos_cffi/functions.py +++ b/pymeos_cffi/pymeos_cffi/functions.py @@ -1,4 +1,6 @@ import logging +import os + from datetime import datetime, timedelta, date from typing import Any, Tuple, Optional, List @@ -203,6 +205,12 @@ def meos_get_intervalstyle() -> str: def meos_initialize(tz_str: "Optional[str]") -> None: + if "PROJ_DATA" not in os.environ and "PROJ_LIB" not in os.environ: + # Assume we are in a wheel and the PROJ data is in the package + proj_dir = os.path.join(os.path.dirname(__file__), "proj_data") + os.environ["PROJ_DATA"] = proj_dir + os.environ["PROJ_LIB"] = proj_dir + tz_str_converted = tz_str.encode("utf-8") if tz_str is not None else _ffi.NULL _lib.meos_initialize(tz_str_converted, _lib.py_error_handler) diff --git a/pymeos_cffi/setup.py b/pymeos_cffi/setup.py index 8d234f42..7681da29 100644 --- a/pymeos_cffi/setup.py +++ b/pymeos_cffi/setup.py @@ -1,7 +1,37 @@ +import os +import shutil + from setuptools import setup + +package_data = [] + +# Conditionally copy PROJ DATA to make self-contained wheels +if os.environ.get("PACKAGE_DATA"): + print("Copying PROJ data to package data") + projdatadir = os.environ.get( + "PROJ_DATA", os.environ.get("PROJ_LIB", "/usr/local/share/proj") + ) + if os.path.exists(projdatadir): + shutil.rmtree("pymeos_cffi/proj_data", ignore_errors=True) + shutil.copytree( + projdatadir, + "pymeos_cffi/proj_data", + ignore=shutil.ignore_patterns("*.txt", "*.tif"), + ) # Don't copy .tiff files and their related .txt files + else: + raise FileNotFoundError( + f"PROJ data directory not found at {projdatadir}. " + f"Unable to generate self-contained wheel." + ) + package_data.append("proj_data/*") +else: + print("Not copying PROJ data to package data") + setup( packages=["pymeos_cffi", "pymeos_cffi.builder"], setup_requires=["cffi"], + include_package_data=True, + package_data={"pymeos_cffi": package_data}, cffi_modules=["pymeos_cffi/builder/build_pymeos.py:ffibuilder"], )