diff --git a/.github/workflows/build-xy.yml b/.github/workflows/build-xy.yml new file mode 100644 index 000000000000..cc7562a9e1bf --- /dev/null +++ b/.github/workflows/build-xy.yml @@ -0,0 +1,204 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# Based on upstream's release build: +# https://github.com/reflex-dev/xy/blob/v0.0.8/.github/workflows/build_release_artifacts.yml +name: Build xy wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/xy.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-xy.yml' + - 'docs/packages/xy.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-xy.yml' + - 'docs/packages/xy.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: xy + version: ${{ inputs.version }} + + build_sdist: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Build xy ${{ matrix.version }} sdist + runs-on: ubuntu-latest + env: + XY_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout xy v${{ env.XY_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: reflex-dev/xy + ref: v${{ env.XY_VERSION }} + fetch-depth: 0 + persist-credentials: false + + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + + - name: Install JS build toolchain + run: npm ci + + - name: Build sdist + run: uv build --sdist + + - name: Verify sdist contents + run: python3 scripts/verify_sdist.py dist/*.tar.gz + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: xy-${{ env.XY_VERSION }}-sdist + path: dist/*.tar.gz + if-no-files-found: error + + build_wheels: + needs: [setup, build_sdist] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Build xy ${{ matrix.version }} py3-none-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 120 + env: + XY_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout xy v${{ env.XY_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: reflex-dev/xy + ref: v${{ env.XY_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + # auditwheel's riscv64 policy floor (manylinux_2_34) sits below the + # manylinux_2_39 build image, so the wheel comes out tagged for both and + # scripts/verify_wheel.py's filename check (a single-string comparison) + # chokes on the resulting compressed tag set (CLAUDE.md gotcha 510). + - name: Patch xy source + run: git apply python-wheels/patches/xy/${{ env.XY_VERSION }}/*.patch + + - name: Download sdist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: xy-${{ env.XY_VERSION }}-sdist + path: dist/ + + # cibuildwheel chdirs into a temp extraction of a .tar.gz package-dir, so + # CIBW_TEST_SOURCES (resolved against the new cwd) would never see the + # tests/ staged next to it here; extract it ourselves and point + # package-dir at the resulting directory instead (gotcha 251/281). + - id: sdist_path + run: | + tar zxf dist/*.tar.gz -C dist + echo "path=$(echo dist/*/ | sed 's:/$::')" >> "$GITHUB_OUTPUT" + + # The Rust core is a ctypes-loaded cdylib, so the wheel is py3-none: one + # build serves every interpreter and the other identifiers only re-test it. + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + package-dir: ${{ steps.sdist_path.outputs.path }} + output-dir: wheelhouse/ + env: + CIBW_BUILD: cp312-manylinux_riscv64 cp313-manylinux_riscv64 cp314-manylinux_riscv64 cp314t-manylinux_riscv64 + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + CIBW_BEFORE_ALL_LINUX: >- + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + CIBW_ENVIRONMENT_LINUX: >- + PATH="$PATH:$HOME/.cargo/bin" + XY_REQUIRE_CARGO=1 + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + # Without it pip takes PyPI's newer sdist-only releases over our + # registry's wheels and source-builds them (gotcha 210). + CIBW_TEST_ENVIRONMENT: PIP_ONLY_BINARY=numpy,pillow + # Upstream's full suite needs Chromium, Node and the repo's scripts/; + # these files cover the native core and the exporters without them. + CIBW_TEST_REQUIRES: pytest hypothesis pillow + CIBW_TEST_SOURCES: tests + CIBW_TEST_COMMAND: >- + python -c "import xy.kernels as k; assert k.BACKEND == 'native', k.BACKEND" && + python -m pytest -q + tests/test_kernels.py + tests/test_import.py + tests/test_lod.py + tests/test_ooc.py + tests/test_jpeg.py + tests/test_webp.py + tests/test_image_export.py + tests/test_png_export.py + tests/test_pdf_export.py + tests/test_spatial.py + tests/test_figure.py + tests/test_channel.py + tests/test_scatter.py + tests/test_streaming.py + tests/test_range_indices_rows.py + tests/test_select_polygon.py + tests/test_density_mean_color.py + + - name: Verify wheel contents + run: python3 scripts/verify_wheel.py wheelhouse/*.whl --expect-native + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: xy-${{ env.XY_VERSION }}-py3-none-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish xy ${{ matrix.version }} + needs: [setup, build_wheels] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: xy-${{ matrix.version }}-py3-none-manylinux_riscv64 diff --git a/docs/packages/xy.yaml b/docs/packages/xy.yaml new file mode 100644 index 000000000000..fe92b86d2a0b --- /dev/null +++ b/docs/packages/xy.yaml @@ -0,0 +1,6 @@ +package-name: xy +source-code: https://github.com/reflex-dev/xy +license: Apache-2.0 +versions: +- version: 0.0.8 + patched: true diff --git a/patches/xy/0.0.8/0001-scripts-accept-a-compressed-manylinux-tag-set-in-ver.patch b/patches/xy/0.0.8/0001-scripts-accept-a-compressed-manylinux-tag-set-in-ver.patch new file mode 100644 index 000000000000..829124e64cff --- /dev/null +++ b/patches/xy/0.0.8/0001-scripts-accept-a-compressed-manylinux-tag-set-in-ver.patch @@ -0,0 +1,60 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 25 Oct 2026 17:57:24 +0000 +Subject: [PATCH] scripts: accept a compressed manylinux tag set in + verify_wheel's filename check + +Upstream-Status: Inappropriate [riscv64 auditwheel policy floor sits below our build image, a shape upstream's own cross-compiled legs never produce] + +auditwheel's riscv64 policy floor is manylinux_2_34, one below the +manylinux_2_39 build image our workflow pins, so `auditwheel repair` +tags the wheel for both and the filename's platform segment becomes a +dot-joined compressed tag set: +`manylinux_2_34_riscv64.manylinux_2_39_riscv64`. WHEEL still lists each +one as its own `Tag:` line, which is what pip reads to decide +installability, but _require_filename_tag compared the whole compressed +string against that list as one token and never matched, failing every +riscv64 build: + + wheel verification failed for + wheelhouse/xy-0.0.8-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl: + wheel filename tag + 'py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64' is not + listed in WHEEL tags + ['py3-none-manylinux_2_34_riscv64', 'py3-none-manylinux_2_39_riscv64'] + +Upstream's own release legs are all single-tag (zig cross-compiles +pinned to one glibc floor per target, no compressed sets), so this +never surfaces there. Split the filename's platform segment on '.' and +require each resulting tag to be listed, which keeps the single-tag +case working exactly as before. +--- + scripts/verify_wheel.py | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/scripts/verify_wheel.py b/scripts/verify_wheel.py +index 609bc45..f6ee611 100644 +--- a/scripts/verify_wheel.py ++++ b/scripts/verify_wheel.py +@@ -154,8 +154,17 @@ def _filename_tag(path: Path) -> str: + + + def _require_filename_tag(path: Path, tags: list[str]) -> None: ++ # auditwheel's riscv64 policy floor (manylinux_2_34) sits below the build ++ # image (manylinux_2_39), so `auditwheel repair` tags the wheel for both: ++ # the filename's platform segment is a dot-joined *compressed tag set* ++ # (`manylinux_2_34_riscv64.manylinux_2_39_riscv64`), while WHEEL lists each ++ # one as its own `Tag:` line. Check every compressed segment individually ++ # instead of the whole filename tag as one string, which never matches. + filename_tag = _filename_tag(path) +- if filename_tag not in tags: ++ python, abi, platform = filename_tag.split("-", 2) ++ filename_tags = [f"{python}-{abi}-{plat}" for plat in platform.split(".")] ++ missing = [tag for tag in filename_tags if tag not in tags] ++ if missing: + raise AssertionError( + f"wheel filename tag {filename_tag!r} is not listed in WHEEL tags {tags}" + ) +-- +2.43.0 +