diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a7deb0e39..6fea17dbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,157 +29,70 @@ jobs: echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT - linux-build: + build-wheels: 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 + os: [ubuntu-latest, macos-latest, macos-15-intel] runs-on: ${{ matrix.os }} - container: ${{ matrix.container }} - env: - UV_LINK_MODE: copy - UV_CACHE_DIR: /github/home/.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: /github/home/.cache/uv - key: uvcache-${{ runner.os }}-${{ matrix.arch_label }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} - restore-keys: | - uvcache-${{ runner.os }}-${{ matrix.arch_label }}- - - - name: Add Debian sid repository - if: matrix.container != null + - name: Install cibuildwheel 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 + python -m pip install --upgrade pip + pip install cibuildwheel - - name: Install system dependencies (Linux) - if: matrix.container != null + - name: Build wheels + env: + CIBW_BUILD_VERBOSITY: 1 + CIBW_BEFORE_BUILD: "pip install uv && uv sync --frozen --no-dev || uv sync --no-dev" run: | - apt-get update - apt-get install -y base-files - apt-get install -y build-essential pkg-config libbz2-dev - apt-get install -y -t sid libmapnik-dev + cibuildwheel --output-dir wheelhouse - - name: Install uv - uses: astral-sh/setup-uv@v7 + - name: Upload wheels + uses: actions/upload-artifact@v4 with: - enable-cache: false + name: wheels-${{ matrix.os }} + path: wheelhouse/*.whl + retention-days: 7 - - 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 + build-sdist: + needs: [extract-version] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: recursive - - name: Build wheel and sdist + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Build sdist run: | - uv build --wheel --sdist + uv build --sdist - - name: Upload build artifacts + - name: Upload sdist uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.platform }} - path: dist/ + name: sdist + path: dist/*.tar.gz 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 + - build-wheels + - build-sdist runs-on: ubuntu-latest permissions: contents: write @@ -194,8 +107,12 @@ jobs: - name: Combine artifacts run: | mkdir -p dist + # Copy all wheels 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/ \; + # Copy sdist (should be only one) + find dist-all -name "*.tar.gz" -exec cp {} dist/ \; + echo "Artifacts in dist/:" + ls -lh dist/ - name: Create GitHub Release uses: softprops/action-gh-release@v2 @@ -250,8 +167,8 @@ jobs: 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 "- 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 4adfe89f8..da462f217 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -176,20 +176,51 @@ jobs: - 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 }} + tools: mapnik icu4c pkg-config boost gdal proj harfbuzz libx11 xorgproto libxext libxrender libxau libxdmcp + key: mapnik-${{ matrix.os }}-${{ matrix.arch_label }}-20260118 - name: Setup pkg-config and build environment run: | + # Ensure Homebrew environment is available + eval $(brew shellenv) + # Get package-specific prefixes using brew --prefix and save to GITHUB_ENV for reuse + HOMEBREW_PREFIX=$(brew --prefix) + MAPNIK_PREFIX=$(brew --prefix mapnik) + ICU4C_PREFIX=$(brew --prefix icu4c) + BOOST_PREFIX=$(brew --prefix boost) + GDAL_PREFIX=$(brew --prefix gdal) + PROJ_PREFIX=$(brew --prefix proj) + HARFBUZZ_PREFIX=$(brew --prefix harfbuzz) + XORGPROTO_PREFIX=$(brew --prefix xorgproto) + # Find the actual xorgproto pkg-config directory (may be in Cellar with version) + XORGPROTO_PKGCONFIG_DIR=$(find "$HOMEBREW_PREFIX/Cellar/xorgproto" -type d -name "pkgconfig" 2>/dev/null | head -1) + if [ -z "$XORGPROTO_PKGCONFIG_DIR" ]; then + # Fallback to opt symlink location + XORGPROTO_PKGCONFIG_DIR="$XORGPROTO_PREFIX/share/pkgconfig" + echo "Warning: xorgproto pkgconfig directory not found in Cellar, using fallback: $XORGPROTO_PKGCONFIG_DIR" + else + echo "Found xorgproto pkgconfig directory: $XORGPROTO_PKGCONFIG_DIR" + fi + # Save prefixes to GITHUB_ENV for reuse in subsequent steps + echo "HOMEBREW_PREFIX=$HOMEBREW_PREFIX" >> $GITHUB_ENV + echo "MAPNIK_PREFIX=$MAPNIK_PREFIX" >> $GITHUB_ENV + echo "ICU4C_PREFIX=$ICU4C_PREFIX" >> $GITHUB_ENV + echo "BOOST_PREFIX=$BOOST_PREFIX" >> $GITHUB_ENV + echo "GDAL_PREFIX=$GDAL_PREFIX" >> $GITHUB_ENV + echo "PROJ_PREFIX=$PROJ_PREFIX" >> $GITHUB_ENV + echo "HARFBUZZ_PREFIX=$HARFBUZZ_PREFIX" >> $GITHUB_ENV + echo "XORGPROTO_PREFIX=$XORGPROTO_PREFIX" >> $GITHUB_ENV + echo "XORGPROTO_PKGCONFIG_DIR=$XORGPROTO_PKGCONFIG_DIR" >> $GITHUB_ENV + echo "Homebrew prefix: $HOMEBREW_PREFIX" + echo "Mapnik prefix: $MAPNIK_PREFIX" + echo "xorgproto pkg-config dir: $XORGPROTO_PKGCONFIG_DIR" # 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 + # This ensures pkg-config can find Mapnik and its dependencies (ICU, Boost, GDAL, PROJ, HarfBuzz, X11 proto, etc.) + # Note: xorgproto .pc files are in Cellar/xorgproto//share/pkgconfig + echo "PKG_CONFIG_PATH=$HOMEBREW_PREFIX/lib/pkgconfig:$HOMEBREW_PREFIX/share/pkgconfig:$ICU4C_PREFIX/lib/pkgconfig:$GDAL_PREFIX/lib/pkgconfig:$PROJ_PREFIX/lib/pkgconfig:$HARFBUZZ_PREFIX/lib/pkgconfig:$XORGPROTO_PKGCONFIG_DIR:$XORGPROTO_PREFIX/share/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" + echo "CXXFLAGS=-I$BOOST_PREFIX/include -I$GDAL_PREFIX/include -I$PROJ_PREFIX/include -I$HARFBUZZ_PREFIX/include $CXXFLAGS" >> $GITHUB_ENV + echo "LDFLAGS=-L$BOOST_PREFIX/lib -L$GDAL_PREFIX/lib -L$PROJ_PREFIX/lib -L$HARFBUZZ_PREFIX/lib $LDFLAGS" >> $GITHUB_ENV - name: Install uv uses: astral-sh/setup-uv@v7 @@ -198,6 +229,13 @@ jobs: - name: Install Python dependencies and build package run: | + # Ensure Homebrew environment is available + eval $(brew shellenv) + # Re-export environment variables (already set in previous step via GITHUB_ENV) + # This ensures they're available in the current shell session for subprocess calls + export PKG_CONFIG_PATH="$PKG_CONFIG_PATH" + export CXXFLAGS="$CXXFLAGS" + export LDFLAGS="$LDFLAGS" # Use lockfile when present; avoid doing two full syncs (which can rebuild twice). if [ -f uv.lock ]; then uv sync --extra test --frozen --verbose @@ -231,22 +269,27 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: coverage-reports-macos-${{ matrix.arch_label }} + name: coverage-reports-macos-${{ matrix.os }}-${{ 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: Publish test results + uses: EnricoMi/publish-unit-test-result-action/macos@v2 + if: always() + with: + files: junit.xml + check_name: Test Results (macOS ${{ matrix.os }} ${{ matrix.arch_label }}) + comment_mode: off + - 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 }} + flags: unittests-macos-${{ matrix.os }}-${{ matrix.arch_label }} + name: python-mapnik-coverage-macos-${{ matrix.os }}-${{ matrix.arch_label }} fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c688ff117..3fd9e9185 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,46 @@ testpaths = [ [tool.uv] # Pin uv version for CI/dev consistency (used by astral-sh/setup-uv). -required-version = ">=0.9.0" \ No newline at end of file +required-version = ">=0.9.0" + +[tool.cibuildwheel] +# Build only Python 3.12+ (matches requires-python) +build = "cp312-*" +skip = ["*-win32", "*-manylinux_i686", "*-musllinux_*"] +test-command = "uv run pytest test/python_tests -v" +test-extras = ["test"] + +# Linux build configuration +[tool.cibuildwheel.linux] +# Use manylinux_2_28 (Debian 12 / Bookworm based) +manylinux-x86_64-image = "manylinux_2_28_x86_64" +manylinux-aarch64-image = "manylinux_2_28_aarch64" + +# Install system dependencies before build +before-all = """ + # Add Debian sid repository for Mapnik 4.2 + 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 + + # Install build dependencies + apt-get update + apt-get install -y build-essential pkg-config libbz2-dev + apt-get install -y -t sid libmapnik-dev fonts-noto-cjk +""" + +# macOS build configuration +[tool.cibuildwheel.macos] +# Build for both x86_64 and arm64 +archs = ["x86_64", "arm64"] + +# Install dependencies via Homebrew (run once before all builds) +before-all = "brew install mapnik icu4c pkg-config boost gdal proj harfbuzz" + +# Set environment variables for each build +# These are exported in the build environment +before-build = """ + eval $(brew shellenv) + export 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 + export 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" + export 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" +"""