diff --git a/.github/workflows/build-anki.yml b/.github/workflows/build-anki.yml new file mode 100644 index 000000000000..f6c38b7a1239 --- /dev/null +++ b/.github/workflows/build-anki.yml @@ -0,0 +1,193 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on: https://github.com/ankitects/anki/blob/26.08.1/.github/workflows/release.yml +name: Build anki wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/anki.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-anki.yml' + - 'docs/packages/anki.yaml' + - 'patches/anki/**' + push: + branches: [main] + paths: + - '.github/workflows/build-anki.yml' + - 'docs/packages/anki.yaml' + - 'patches/anki/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: anki + version: ${{ inputs.version }} + + build_wheel: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Build anki ${{ matrix.version }} cp310-abi3-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + container: + image: quay.io/pypa/manylinux_2_39_riscv64 + defaults: + run: + shell: bash + env: + ANKI_VERSION: ${{ matrix.version }} + RELEASE: 2 + + steps: + - name: Resolve the anki tag + id: tag + run: | + IFS=. read -r year month patch <<< "$ANKI_VERSION" + echo "tag=${year}.$(printf %02d "$month")${patch:+.$patch}" >> "$GITHUB_OUTPUT" + + - name: Checkout anki ${{ steps.tag.outputs.tag }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ankitects/anki + ref: ${{ steps.tag.outputs.tag }} + submodules: true + persist-credentials: false + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch anki source + run: git apply python-wheels/patches/anki/${{ env.ANKI_VERSION }}/00*.patch + + - name: Setup build environment + run: | + git config --global --add safe.directory '*' + dnf -y install protobuf-compiler + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain none --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + "$HOME/.cargo/bin/rustup" toolchain install + PATH="$HOME/.cargo/bin:$PATH" tools/install-n2 + + # Upstream's pyenv is a `uv sync --extra qt` of the whole workspace, which + # needs PyQt6/WebEngine; OFFLINE_BUILD is upstream's documented way to + # supply the venv instead, and pylib only needs protoc-gen-mypy from it. + - name: Create the Python environment + run: | + /opt/python/cp313-cp313/bin/python -m venv out/pyenv + out/pyenv/bin/pip install mypy-protobuf==3.6.0 uv==0.11.8 + + # pylib/ has no LICENSE of its own, so hatchling's licence glob finds nothing + # and upstream's wheels ship none. + - name: Stage licence + run: cp LICENSE pylib/ + + - name: Build wheel + env: + OFFLINE_BUILD: '1' + PROTOC_BINARY: /usr/bin/protoc + UV_BINARY: ${{ github.workspace }}/out/pyenv/bin/uv + UV_PYTHON: ${{ github.workspace }}/out/pyenv/bin/python + run: ./ninja wheels:anki + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: anki-${{ env.ANKI_VERSION }}-cp310-abi3-manylinux_riscv64 + path: out/wheels/anki-*.whl + if-no-files-found: error + + test_wheel: + needs: [setup, build_wheel] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + python: ["cp312", "cp313", "cp314"] + name: Test anki ${{ matrix.version }} ${{ matrix.python }} + runs-on: ubuntu-24.04-riscv + timeout-minutes: 60 + container: + image: quay.io/pypa/manylinux_2_39_riscv64 + defaults: + run: + shell: bash + env: + ANKI_VERSION: ${{ matrix.version }} + + steps: + - name: Resolve the anki tag + id: tag + run: | + IFS=. read -r year month patch <<< "$ANKI_VERSION" + echo "tag=${year}.$(printf %02d "$month")${patch:+.$patch}" >> "$GITHUB_OUTPUT" + + - name: Checkout anki ${{ steps.tag.outputs.tag }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ankitects/anki + ref: ${{ steps.tag.outputs.tag }} + persist-credentials: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: anki-${{ env.ANKI_VERSION }}-cp310-abi3-manylinux_riscv64 + path: dist + + - name: Install wheel + env: + PIP_EXTRA_INDEX_URL: https://pypi.riseproject.dev/simple/ + PIP_ONLY_BINARY: orjson,protobuf + run: | + /opt/python/${{ matrix.python }}-${{ matrix.python }}/bin/python -m venv /venv + /venv/bin/pip install dist/anki-*.whl pytest==8.4.1 + /venv/bin/python -c "import anki._rsbridge as m; assert m.__file__.endswith('.so'), m.__file__" + + # Upstream's check:pytest:pylib. Staged out of the checkout so pylib/anki does + # not shadow the wheel; test_create_open expects '/' to be unwritable, and the + # container runs as root. + - name: Run pylib tests + run: | + mkdir /stage + cp -a pylib/tests /stage/ + cd /stage + ANKI_TEST_MODE=1 /venv/bin/python -m pytest -p no:cacheprovider tests \ + --deselect tests/test_collection.py::test_create_open + + publish: + name: Publish anki ${{ matrix.version }} + needs: [setup, build_wheel, test_wheel] + 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: anki-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/anki.yaml b/docs/packages/anki.yaml new file mode 100644 index 000000000000..3a40991293e0 --- /dev/null +++ b/docs/packages/anki.yaml @@ -0,0 +1,5 @@ +package-name: anki +source-code: https://github.com/ankitects/anki +license: AGPL-3.0-or-later +versions: +- version: 26.8.1 diff --git a/patches/anki/26.8.1/0001-build-add-riscv64-linux-platform.patch b/patches/anki/26.8.1/0001-build-add-riscv64-linux-platform.patch new file mode 100644 index 000000000000..9fcda6ec4a30 --- /dev/null +++ b/patches/anki/26.8.1/0001-build-add-riscv64-linux-platform.patch @@ -0,0 +1,115 @@ +Upstream-Status: To upstream [not yet submitted] + +Add a riscv64 Linux platform to the build system + +Platform::current() panics on any arch other than x86_64/aarch64, so +configure cannot run on riscv64 at all. Add a LinuxRiscv variant: rustls +like the other Linux targets, the riscv64gc Rust triple, uv's riscv64gc +release archive, and a manylinux_2_39_riscv64 wheel tag. protoc and node +publish no riscv64 binary, so protoc requires PROTOC_BINARY, and node +reuses the x86_64 entry (only reached without NODE_BINARY), the same way +LinuxArm already reuses the x86_64 clang-format binary. + +diff --git a/build/configure/src/python.rs b/build/configure/src/python.rs +index 947582f..a8e4fe8 100644 +--- a/build/configure/src/python.rs ++++ b/build/configure/src/python.rs +@@ -155,6 +155,7 @@ impl BuildAction for BuildWheel { + let platform_tag = match platform { + Platform::LinuxX64 => "manylinux_2_35_x86_64", + Platform::LinuxArm => "manylinux_2_35_aarch64", ++ Platform::LinuxRiscv => "manylinux_2_39_riscv64", + Platform::MacX64 => "macosx_12_0_x86_64", + Platform::MacArm => "macosx_12_0_arm64", + Platform::WindowsX64 => "win_amd64", +diff --git a/build/ninja_gen/src/archives.rs b/build/ninja_gen/src/archives.rs +index 3d2120b..ca6852d 100644 +--- a/build/ninja_gen/src/archives.rs ++++ b/build/ninja_gen/src/archives.rs +@@ -23,6 +23,7 @@ pub struct OnlineArchive { + pub enum Platform { + LinuxX64, + LinuxArm, ++ LinuxRiscv, + MacX64, + MacArm, + WindowsX64, +@@ -36,6 +37,7 @@ impl Platform { + match (os, arch) { + ("linux", "x86_64") => Self::LinuxX64, + ("linux", "aarch64") => Self::LinuxArm, ++ ("linux", "riscv64") => Self::LinuxRiscv, + ("macos", "x86_64") => Self::MacX64, + ("macos", "aarch64") => Self::MacArm, + ("windows", "x86_64") => Self::WindowsX64, +@@ -49,7 +51,7 @@ impl Platform { + // On Linux, wheels are not allowed to link to OpenSSL, and linking setup + // caused pain for AnkiDroid in the past. On other platforms, we stick to + // native libraries, for smaller binaries. +- Platform::LinuxX64 | Platform::LinuxArm => "rustls", ++ Platform::LinuxX64 | Platform::LinuxArm | Platform::LinuxRiscv => "rustls", + _ => "native-tls", + } + } +@@ -58,6 +60,7 @@ impl Platform { + match self { + Platform::LinuxX64 => "x86_64-unknown-linux-gnu", + Platform::LinuxArm => "aarch64-unknown-linux-gnu", ++ Platform::LinuxRiscv => "riscv64gc-unknown-linux-gnu", + Platform::MacX64 => "x86_64-apple-darwin", + Platform::MacArm => "aarch64-apple-darwin", + Platform::WindowsX64 => "x86_64-pc-windows-msvc", +diff --git a/build/ninja_gen/src/node.rs b/build/ninja_gen/src/node.rs +index b6731e2..608f29b 100644 +--- a/build/ninja_gen/src/node.rs ++++ b/build/ninja_gen/src/node.rs +@@ -18,7 +18,8 @@ use crate::input::BuildInput; + + pub fn node_archive(platform: Platform) -> OnlineArchive { + match platform { +- Platform::LinuxX64 => OnlineArchive { ++ // todo: nodejs.org publishes no riscv64 build; set NODE_BINARY ++ Platform::LinuxX64 | Platform::LinuxRiscv => OnlineArchive { + url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-x64.tar.xz", + sha256: "325c0f1261e0c61bcae369a1274028e9cfb7ab7949c05512c5b1e630f7e80e12", + }, +diff --git a/build/ninja_gen/src/protobuf.rs b/build/ninja_gen/src/protobuf.rs +index 1198f65..eaba26b 100644 +--- a/build/ninja_gen/src/protobuf.rs ++++ b/build/ninja_gen/src/protobuf.rs +@@ -31,6 +31,9 @@ pub fn protoc_archive(platform: Platform) -> OnlineArchive { + sha256: "6c554de11cea04c56ebf8e45b54434019b1cd85223d4bbd25c282425e306ecc2", + } + }, ++ Platform::LinuxRiscv => { ++ panic!("protoc publishes no riscv64 binary; set PROTOC_BINARY") ++ } + Platform::MacX64 | Platform::MacArm => { + OnlineArchive { + url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-osx-universal_binary.zip", +@@ -54,7 +57,7 @@ fn clang_format_archive(platform: Platform) -> OnlineArchive { + sha256: "64060bc4dbca30d0d96aab9344e2783008b16e1cae019a2532f1126ca5ec5449", + } + } +- Platform::LinuxArm => { ++ Platform::LinuxArm | Platform::LinuxRiscv => { + // todo: replace with arm64 binary + OnlineArchive { + url: "https://github.com/ankitects/clang-format-binaries/releases/download/anki-2021-01-09/clang-format_linux_x86_64.zip", +diff --git a/build/ninja_gen/src/python.rs b/build/ninja_gen/src/python.rs +index 16c61f7..755ca13 100644 +--- a/build/ninja_gen/src/python.rs ++++ b/build/ninja_gen/src/python.rs +@@ -37,6 +37,12 @@ pub fn uv_archive(platform: Platform) -> OnlineArchive { + sha256: "eee8dd658d20e5ac85fec9c2326b6cbc9d83a1eef09ef07433e58698ac849591", + } + }, ++ Platform::LinuxRiscv => { ++ OnlineArchive { ++ url: "https://github.com/astral-sh/uv/releases/download/0.11.8/uv-riscv64gc-unknown-linux-gnu.tar.gz", ++ sha256: "dd43289c567fda3ca59ec714ffca09125f1149289448667f36a4bb7c29c859be", ++ } ++ }, + Platform::MacX64 => { + OnlineArchive { + url: "https://github.com/astral-sh/uv/releases/download/0.11.8/uv-x86_64-apple-darwin.tar.gz",