Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ jobs:
strategy:
matrix:
platform:
- runner: macos-13
- runner: macos-15-intel
target: x86_64
- runner: macos-14
- runner: macos-15
target: aarch64
steps:
- uses: actions/checkout@v4
Expand Down
154 changes: 133 additions & 21 deletions .github/workflows/build_and_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,161 @@ name: Build and Publish Wheels

on:
workflow_dispatch:
inputs:
publish:
description: Publish the verified artifacts to PyPI
required: true
default: false
type: boolean

permissions:
contents: read

concurrency:
group: build-and-publish-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
wheels:
name: Wheels (${{ matrix.os }} / ${{ matrix.archs }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
archs: "x86_64 aarch64"
qemu: true
artifact: wheels-linux
- os: macos-15-intel
archs: "x86_64"
qemu: false
artifact: wheels-macos-x86_64
- os: macos-15
archs: "arm64"
qemu: false
artifact: wheels-macos-arm64
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v7
with:
python-version: "3.12"

- name: Set up QEMU (for ARM64 emulation)
- name: Set up QEMU for Linux ARM64
if: matrix.qemu
uses: docker/setup-qemu-action@v3
with:
platforms: "linux/arm64/v8"

- name: Install build tools (Maturin & cibuildwheel)
run: |
python -m pip install --upgrade pip
python -m pip install maturin cibuildwheel
- name: Install cibuildwheel
run: python -m pip install --upgrade pip cibuildwheel==4.2.0

- name: Build wheels (ManyLinux 2_28 for x86_64 & aarch64)
- name: Build and test wheels
env:
CIBW_ARCHS: "x86_64 aarch64" # target architectures
CIBW_BUILD: "cp312-* cp313-*" # build only CPython 3.12 and 3.13
CIBW_SKIP: "pp* *-musllinux_*" # skip PyPy and musllinux builds
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 # use latest ManyLinux 2_28 image (x86_64)
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 # use latest ManyLinux 2_28 image (aarch64)
CIBW_BEFORE_ALL_LINUX: dnf install -y cargo rustc # install Rust toolchain in container
run: python -m cibuildwheel --platform linux --output-dir dist
CIBW_ARCHS: ${{ matrix.archs }}
CIBW_BUILD: "cp312-* cp313-*"
CIBW_SKIP: "*-musllinux_*"
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
CIBW_BEFORE_ALL_LINUX: dnf install -y cargo rustc
CIBW_TEST_REQUIRES: numpy
CIBW_TEST_COMMAND: >-
python -c "import numpy as np, ruranges.numpy as k;
s=np.array([1,5],dtype=np.int64); e=np.array([4,9],dtype=np.int64);
g=np.zeros(2,dtype=np.uint32);
assert len(k.merge(starts=s,ends=e,groups=g,slack=0)[0])==2;
print('ruranges ok')"
run: python -m cibuildwheel --output-dir dist

- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: dist/*.whl
if-no-files-found: error

sdist:
name: Source distribution
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.12"

- name: Build sdist
run: python -m pip install --upgrade pip build && python -m build --sdist --outdir dist

- name: Upload sdist
uses: actions/upload-artifact@v7
with:
name: sdist
path: dist/*.tar.gz
if-no-files-found: error

publish:
name: Verify and publish
needs: [wheels, sdist]
runs-on: ubuntu-latest
steps:
- name: Download distributions
uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true

- name: Verify distribution set
run: |
python -m pip install --upgrade pip
python -m pip install build
python -m build --sdist --outdir dist
ls -l dist
python - <<'PY'
from pathlib import Path

dist = Path("dist")
files = sorted(path.name for path in dist.iterdir() if path.is_file())
wheels = [name for name in files if name.endswith(".whl")]
sdists = [name for name in files if name.endswith(".tar.gz")]

assert len(files) == 9, f"expected 9 distributions, found {len(files)}: {files}"
assert len(wheels) == 8, f"expected 8 wheels, found {len(wheels)}: {wheels}"
assert len(sdists) == 1, f"expected 1 sdist, found {len(sdists)}: {sdists}"

for python_tag in ("cp312-cp312", "cp313-cp313"):
expected_platforms = (
"manylinux_2_28_x86_64",
"manylinux_2_28_aarch64",
)
for platform_tag in expected_platforms:
assert any(
f"-{python_tag}-{platform_tag}.whl" in name for name in wheels
), f"missing {python_tag} {platform_tag} wheel: {wheels}"

assert any(
f"-{python_tag}-macosx_" in name and name.endswith("_x86_64.whl")
for name in wheels
), f"missing {python_tag} macOS x86_64 wheel: {wheels}"
assert any(
f"-{python_tag}-macosx_" in name and name.endswith("_arm64.whl")
for name in wheels
), f"missing {python_tag} macOS arm64 wheel: {wheels}"

print("Verified distribution set:")
print("\n".join(files))
PY

- name: Dry-run summary
if: ${{ !inputs.publish }}
run: echo "Artifacts verified; PyPI publishing was not requested."

- name: Publish to PyPI
if: ${{ inputs.publish }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: dist
skip-existing: true
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[package]
name = "ruranges-py"
version = "0.2.6"
# Keep this synchronized with `project.version` in pyproject.toml.
version = "0.2.7"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ ruranges-py is the Python bindings package for `ruranges-core`, a separate Rust
## Installation

```bash
pip install ruranges-py # PyPI
pip install ruranges # PyPI
# or
pip install git+https://github.com/your-org/ruranges-py.git
pip install git+https://github.com/pyranges/ruranges_py.git
```

### Development environment (from local checkout)

```bash
cd ~/code
git clone <your-remote>/ruranges-py
git clone https://github.com/pyranges/ruranges_py.git

cd ~/code/ruranges-py
cd ruranges_py
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
Expand All @@ -40,6 +40,14 @@ Quick check:
python -c "import ruranges; print(ruranges.__version__)"
```

### Release versioning

The Python distribution and its Rust extension crate are released as one
artifact. Keep `[project].version` in `pyproject.toml` and `[package].version`
in `Cargo.toml` identical, and update both in the same release commit. The
publishing workflow deliberately fails if that version already exists on
PyPI, so every release must use a new version.

---

## Cheat sheet
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ classifiers = [
"Operating System :: OS Independent",
]

version = "0.1.8"
# Keep this synchronized with `package.version` in Cargo.toml.
version = "0.2.7"

[tool.maturin]
module-name = "ruranges.ruranges"
Expand All @@ -30,4 +31,4 @@ manylinux = "manylinux_2_28"
strip = true

[project.urls]
Repository = "https://github.com/pyranges/ruranges-py"
Repository = "https://github.com/pyranges/ruranges_py"
Loading