From 2e0895968b007c0184292ac2350433b9c4b89f94 Mon Sep 17 00:00:00 2001 From: scorix Date: Sun, 18 Jan 2026 13:11:22 +0800 Subject: [PATCH] ci: test across platform --- .github/workflows/release.yml | 193 +++++++++++++++++++++++++++------ .github/workflows/test.yml | 169 ++++++++++++++++++++++++++--- src/mapnik_value_converter.hpp | 17 ++- 3 files changed, 327 insertions(+), 52 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85b81bcc5..a7deb0e39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,14 +12,41 @@ on: type: string jobs: - test: - uses: ./.github/workflows/test.yml - - build: - needs: test + extract-version: runs-on: ubuntu-latest - container: - image: python:3.12-bookworm + outputs: + version: ${{ steps.version.outputs.version }} + version_number: ${{ steps.version.outputs.version_number }} + steps: + - name: Extract version from tag + id: version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION=${{ inputs.tag }} + else + VERSION=${GITHUB_REF#refs/tags/} + fi + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT + + linux-build: + needs: [extract-version] + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + container: python:3.12-bookworm + platform: linux_x86_64 + arch: x86_64 + arch_label: x86_64 + - os: ubuntu-24.04-arm + container: python:3.12-bookworm + platform: linux_aarch64 + arch: arm64 + arch_label: arm64 + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} env: UV_LINK_MODE: copy UV_CACHE_DIR: /github/home/.cache/uv @@ -37,30 +64,20 @@ jobs: uses: actions/cache@v5 with: path: /github/home/.cache/uv - key: uvcache-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} + key: uvcache-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} restore-keys: | - uvcache-${{ runner.os }}- - - - name: Extract version from tag - id: version - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - VERSION=${{ inputs.tag }} - else - VERSION=${GITHUB_REF#refs/tags/} - fi - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT + uvcache-${{ runner.os }}-${{ matrix.arch_label }}- - name: Add Debian sid repository + if: matrix.container != null run: | echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list.d/sid.list echo 'Package: *\nPin: release a=sid\nPin-Priority: 100' > /etc/apt/preferences.d/sid - - name: Install system dependencies + - name: Install system dependencies (Linux) + if: matrix.container != null run: | apt-get update - # Ensure /etc/os-release exists (required by some actions' OS detection). apt-get install -y base-files apt-get install -y build-essential pkg-config libbz2-dev apt-get install -y -t sid libmapnik-dev @@ -70,6 +87,25 @@ jobs: with: enable-cache: false + - name: Update version from tag + run: | + # Extract base version from pyproject.toml + BASE_VERSION=$(grep -E '^version = ' pyproject.toml | sed -E 's/^version = "([^"]+)"/\1/') + TAG=${{ needs.extract-version.outputs.version }} + VERSION="${BASE_VERSION}-${TAG}" + echo "Base version from pyproject.toml: $BASE_VERSION" + echo "Git tag: $TAG" + echo "Setting version to: $VERSION" + # Update pyproject.toml + sed -i "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml + # Update setup.py + sed -i "s/version=\"[^\"]*\"/version=\"$VERSION\"/" setup.py + # Verify changes + echo "pyproject.toml version:" + grep "^version = " pyproject.toml + echo "setup.py version:" + grep "version=" setup.py | head -1 + - name: Build wheel and sdist run: | uv build --wheel --sdist @@ -77,44 +113,123 @@ jobs: - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: dist + name: dist-${{ matrix.platform }} path: dist/ retention-days: 7 + # macos-build: + # needs: [extract-version] + # strategy: + # fail-fast: false + # matrix: + # include: + # - os: macos-15-intel + # platform: macosx_10_9_x86_64 + # arch: x86_64 + # arch_label: x86_64 + # - os: macos-15 + # platform: macosx_15_0_arm64 + # arch: arm64 + # arch_label: arm64 + # runs-on: ${{ matrix.os }} + # env: + # UV_LINK_MODE: copy + # UV_CACHE_DIR: /Users/runner/.cache/uv + # permissions: + # actions: write + # contents: write + + # steps: + # - name: Checkout code + # uses: actions/checkout@v6 + # with: + # submodules: recursive + + # - name: Cache uv downloads + # uses: actions/cache@v5 + # with: + # path: /Users/runner/.cache/uv + # key: uvcache-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} + # restore-keys: | + # uvcache-${{ runner.os }}-${{ matrix.arch_label }}- + + # - name: Install and cache Homebrew tools + # uses: tecolicom/actions-use-homebrew-tools@v1 + # with: + # tools: mapnik icu4c pkg-config + # key: mapnik-${{ matrix.arch_label }} + + # - name: Install uv + # uses: astral-sh/setup-uv@v7 + # with: + # enable-cache: false + + # - name: Build wheel and sdist + # run: | + # uv build --wheel --sdist + + # - name: Upload build artifacts + # uses: actions/upload-artifact@v4 + # with: + # name: dist-${{ matrix.platform }} + # path: dist/ + # retention-days: 7 + + release: + needs: + - extract-version + - linux-build + # - macos-build + runs-on: ubuntu-latest + permissions: + contents: write + if: github.event_name == 'push' + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist-all + + - name: Combine artifacts + run: | + mkdir -p dist + find dist-all -name "*.whl" -exec cp {} dist/ \; + find dist-all -name "*.tar.gz" -exec head -1 {} \; | head -1 | xargs -I {} find dist-all -name "{}" -exec cp {} dist/ \; + - name: Create GitHub Release uses: softprops/action-gh-release@v2 - if: github.event_name == 'push' with: - tag_name: ${{ steps.version.outputs.version }} - name: Release ${{ steps.version.outputs.version }} + tag_name: ${{ needs.extract-version.outputs.version }} + name: Release ${{ needs.extract-version.outputs.version }} body: | - ## Python Mapnik ${{ steps.version.outputs.version_number }} + ## Python Mapnik ${{ needs.extract-version.outputs.version_number }} ### Installation - Download the wheel file below and install: + Download the wheel file for your platform and install: ```bash - pip install mapnik-${{ steps.version.outputs.version_number }}-*.whl + pip install mapnik-${{ needs.extract-version.outputs.version_number }}-*.whl ``` - Or install directly from GitHub: + Or install directly from GitHub (replace platform tag as needed): ```bash - pip install https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/mapnik-${{ steps.version.outputs.version_number }}-cp312-cp312-linux_x86_64.whl + pip install https://github.com/${{ github.repository }}/releases/download/${{ needs.extract-version.outputs.version }}/mapnik-${{ needs.extract-version.outputs.version_number }}-cp312-cp312-linux_x86_64.whl ``` Or install from source: ```bash - pip install git+https://github.com/${{ github.repository }}.git@${{ steps.version.outputs.version }} + pip install git+https://github.com/${{ github.repository }}.git@${{ needs.extract-version.outputs.version }} ``` ### Requirements - - Python >= 3.8 - - Mapnik >= 4.0.0 + - Python >= 3.12 + - Mapnik >= 4.2.0 ### Changes - See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ steps.version.outputs.version }}/CHANGELOG.md) for details. + See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ needs.extract-version.outputs.version }}/CHANGELOG.md) for details. files: | dist/*.whl dist/*.tar.gz @@ -127,10 +242,16 @@ jobs: run: | echo "## Release Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "✅ Released version: **${{ steps.version.outputs.version }}**" >> $GITHUB_STEP_SUMMARY + echo "✅ Released version: **${{ needs.extract-version.outputs.version }}**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Artifacts Built:" >> $GITHUB_STEP_SUMMARY ls -lh dist/ >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + echo "### Supported Platforms:" >> $GITHUB_STEP_SUMMARY + echo "- Linux x86_64: ✅" >> $GITHUB_STEP_SUMMARY + echo "- Linux arm64: ✅" >> $GITHUB_STEP_SUMMARY + # echo "- macOS x86_64: ✅" >> $GITHUB_STEP_SUMMARY + # echo "- macOS arm64: ✅" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY echo "### Distribution:" >> $GITHUB_STEP_SUMMARY echo "- GitHub Release: ✅" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b32a9a160..4adfe89f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,10 +9,23 @@ on: workflow_call: jobs: - test: - runs-on: ubuntu-latest - container: - image: python:3.12-bookworm + test-linux: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + container: python:3.12-bookworm + platform: linux_x86_64 + arch: x86_64 + arch_label: x86_64 + - os: ubuntu-24.04-arm + container: python:3.12-bookworm + platform: linux_aarch64 + arch: arm64 + arch_label: arm64 + runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} env: UV_LINK_MODE: copy UV_CACHE_DIR: /github/home/.cache/uv @@ -31,18 +44,18 @@ jobs: uses: actions/cache@v5 with: path: /github/home/.cache/uv - key: uvcache-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} + key: uvcache-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} restore-keys: | - uvcache-${{ runner.os }}- + uvcache-${{ runner.os }}-${{ matrix.arch_label }}- - name: Cache uv virtual environment and build artifacts uses: actions/cache@v5 with: path: .venv - key: venv-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }}-${{ hashFiles('src/**/*.cpp', 'src/**/*.hpp', 'packaging/**/*.py') }} + key: venv-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }}-${{ hashFiles('src/**/*.cpp', 'src/**/*.hpp', 'packaging/**/*.py') }} restore-keys: | - venv-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }}- - venv-${{ runner.os }}- + venv-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }}- + venv-${{ runner.os }}-${{ matrix.arch_label }}- - name: Add Debian sid repository run: | @@ -86,7 +99,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: coverage-reports + name: coverage-reports-linux-${{ matrix.arch_label }} path: | coverage.xml htmlcov/ @@ -98,14 +111,142 @@ jobs: if: always() with: files: junit.xml - check_name: Test Results + check_name: Test Results (Linux ${{ matrix.arch_label }}) comment_mode: off - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 if: always() with: - files: ./coverage.xml - flags: unittests - name: python-mapnik-coverage + files: coverage.xml + flags: unittests-linux-${{ matrix.arch_label }} + name: python-mapnik-coverage-linux-${{ matrix.arch_label }} fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + + test-macos: + strategy: + fail-fast: false + matrix: + include: + - os: macos-15-intel + platform: macosx_15_0_x86_64 + arch: x86_64 + arch_label: x86_64 + - os: macos-15 + platform: macosx_15_0_arm64 + arch: arm64 + arch_label: arm64 + - os: macos-latest + platform: macosx_15_0_arm64 + arch: arm64 + arch_label: arm64 + runs-on: ${{ matrix.os }} + env: + UV_LINK_MODE: copy + UV_CACHE_DIR: /Users/runner/.cache/uv + permissions: + actions: write + contents: read + checks: write + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Cache uv downloads + uses: actions/cache@v5 + with: + path: /Users/runner/.cache/uv + key: uvcache-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} + restore-keys: | + uvcache-${{ runner.os }}-${{ matrix.arch_label }}- + + - name: Cache uv virtual environment and build artifacts + uses: actions/cache@v5 + with: + path: .venv + key: venv-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }}-${{ hashFiles('src/**/*.cpp', 'src/**/*.hpp', 'packaging/**/*.py') }} + restore-keys: | + venv-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }}- + venv-${{ runner.os }}-${{ matrix.arch_label }}- + + - name: Install and cache Homebrew tools + uses: tecolicom/actions-use-homebrew-tools@v1 + with: + tools: mapnik icu4c pkg-config boost gdal proj harfbuzz + key: mapnik-${{ matrix.arch_label }} + + - name: Setup pkg-config and build environment + run: | + # Set PKG_CONFIG_PATH to include Homebrew's pkg-config directories + # This ensures pkg-config can find Mapnik and its dependencies (ICU, Boost, GDAL, PROJ, HarfBuzz, etc.) + echo "PKG_CONFIG_PATH=$(brew --prefix)/lib/pkgconfig:$(brew --prefix)/opt/icu4c/lib/pkgconfig:$(brew --prefix)/opt/boost/lib/pkgconfig:$(brew --prefix)/opt/gdal/lib/pkgconfig:$(brew --prefix)/opt/proj/lib/pkgconfig:$(brew --prefix)/opt/harfbuzz/lib/pkgconfig:$PKG_CONFIG_PATH" >> $GITHUB_ENV + # Set CXXFLAGS and LDFLAGS to include Boost, GDAL, PROJ, and HarfBuzz headers and libraries + echo "CXXFLAGS=-I$(brew --prefix)/opt/boost/include -I$(brew --prefix)/opt/gdal/include -I$(brew --prefix)/opt/proj/include -I$(brew --prefix)/opt/harfbuzz/include $CXXFLAGS" >> $GITHUB_ENV + echo "LDFLAGS=-L$(brew --prefix)/opt/boost/lib -L$(brew --prefix)/opt/gdal/lib -L$(brew --prefix)/opt/proj/lib -L$(brew --prefix)/opt/harfbuzz/lib $LDFLAGS" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH set to: $PKG_CONFIG_PATH" + echo "CXXFLAGS set to: $CXXFLAGS" + echo "LDFLAGS set to: $LDFLAGS" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: false + + - name: Install Python dependencies and build package + run: | + # Use lockfile when present; avoid doing two full syncs (which can rebuild twice). + if [ -f uv.lock ]; then + uv sync --extra test --frozen --verbose + else + uv sync --extra test --verbose + fi + + - name: Run tests with coverage + run: | + uv run pytest test/python_tests -v \ + --cov=mapnik \ + --cov-report=term \ + --cov-report=xml:coverage.xml \ + --cov-report=html:htmlcov \ + --junitxml=junit.xml + + - name: Verify coverage files + if: always() + run: | + pwd + ls -la coverage.xml || echo "coverage.xml not found" + if [ -f coverage.xml ]; then + echo "✅ coverage.xml exists" + head -20 coverage.xml + else + echo "❌ coverage.xml not found" + exit 1 + fi + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-reports-macos-${{ matrix.arch_label }} + path: | + coverage.xml + htmlcov/ + junit.xml + retention-days: 30 + + # Note: publish-unit-test-result-action doesn't support macOS (container action limitation) + # Test results are still uploaded as artifacts above + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + if: always() + with: + files: coverage.xml + flags: unittests-macos-${{ matrix.arch_label }} + name: python-mapnik-coverage-macos-${{ matrix.arch_label }} + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/src/mapnik_value_converter.hpp b/src/mapnik_value_converter.hpp index 2a785aaf3..6b1235c7f 100644 --- a/src/mapnik_value_converter.hpp +++ b/src/mapnik_value_converter.hpp @@ -30,13 +30,26 @@ //pybind11 #include +// Platform-specific macros for Python long conversion +#ifdef __APPLE__ +#define PY_LONG_AS_INTEGER PyLong_AsLongLong +#define PY_LONG_FROM_INTEGER PyLong_FromLongLong +#elif defined(__linux__) +#define PY_LONG_AS_INTEGER PyLong_AsLong +#define PY_LONG_FROM_INTEGER PyLong_FromLong +#else +// Default to AsLongLong for other platforms +#define PY_LONG_AS_INTEGER PyLong_AsLongLong +#define PY_LONG_FROM_INTEGER PyLong_FromLongLong +#endif + namespace { struct value_converter { PyObject * operator() (mapnik::value_integer val) const { - return ::PyLong_FromLong(val); + return ::PY_LONG_FROM_INTEGER(val); } PyObject * operator() (mapnik::value_double val) const @@ -180,7 +193,7 @@ struct type_caster { PyObject *tmp = PyNumber_Long(source); if (!tmp) return false; - value = PyLong_AsLong(tmp); + value = PY_LONG_AS_INTEGER(tmp); Py_DecRef(tmp); return !PyErr_Occurred(); }