Skip to content

Commit 7188ea0

Browse files
authored
Merge pull request #57 from allenai/rust
Rust + CI etc
2 parents 849ccf7 + d77dcc9 commit 7188ea0

21 files changed

Lines changed: 3771 additions & 737 deletions

.github/workflows/main.yaml

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ jobs:
4747
- name: Setup uv
4848
uses: astral-sh/setup-uv@v3
4949

50+
- name: Setup Rust
51+
uses: dtolnay/rust-toolchain@stable
52+
53+
- name: Cache cargo build
54+
uses: actions/cache@v4
55+
with:
56+
path: |
57+
~/.cargo/registry
58+
~/.cargo/git
59+
s2and_rust/target
60+
key: cargo-${{ runner.os }}-${{ hashFiles('s2and_rust/Cargo.lock') }}
61+
restore-keys: |
62+
cargo-${{ runner.os }}-
63+
5064
# Optional: ensure a specific Python (uv can also manage this on its own)
5165
- name: Setup Python
5266
uses: actions/setup-python@v5
@@ -70,24 +84,28 @@ jobs:
7084
shell: bash
7185
run: |
7286
if [[ -f uv.lock ]]; then
73-
uv sync --all-extras --dev --frozen
87+
uv sync --extra dev --frozen
7488
else
7589
# No lock present; resolve once, then install
76-
uv sync --all-extras --dev
90+
uv sync --extra dev
7791
fi
7892
93+
# Build/install Rust extension for parity tests
94+
- name: Build Rust extension
95+
run: uvx --from maturin maturin develop -m s2and_rust/Cargo.toml
96+
7997
# Type checking (run mypy commands directly)
8098
- name: mypy (s2and)
81-
run: uv run mypy s2and --ignore-missing-imports
99+
run: uv run --no-project mypy s2and --ignore-missing-imports
82100
- name: mypy (scripts)
83-
run: uv run mypy scripts/*.py --ignore-missing-imports
101+
run: uv run --no-project mypy scripts/*.py --ignore-missing-imports
84102

85103
# Single pytest run with coverage (replaces the two docker pytest calls)
86104
- name: pytest (coverage)
87105
env:
88106
# keep startup lean; avoid user-level plugins on hosted runners
89107
PYTHONPATH: .
90108
run: |
91-
uv run pytest tests/ \
109+
uv run --no-project pytest tests/ \
92110
--cov=s2and --cov-report=term-missing --cov-fail-under=40
93111

.github/workflows/release-rust.yml

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
name: build-and-publish
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, labeled, unlabeled]
6+
push:
7+
branches: [main]
8+
workflow_dispatch:
9+
inputs:
10+
force_build:
11+
description: "Force build jobs (manual/PR testing)"
12+
required: false
13+
type: boolean
14+
publish_s2and:
15+
description: "Publish s2and to PyPI (manual run)"
16+
required: false
17+
type: boolean
18+
publish_rust:
19+
description: "Publish s2and-rust to PyPI (manual run)"
20+
required: false
21+
type: boolean
22+
23+
jobs:
24+
detect-versions:
25+
name: detect version changes
26+
runs-on: ubuntu-latest
27+
outputs:
28+
s2and_changed: ${{ steps.detect.outputs.s2and_changed }}
29+
rust_changed: ${{ steps.detect.outputs.rust_changed }}
30+
publish_any: ${{ steps.detect.outputs.publish_any }}
31+
publish_s2and: ${{ steps.detect.outputs.publish_s2and }}
32+
publish_rust: ${{ steps.detect.outputs.publish_rust }}
33+
force_build: ${{ steps.detect.outputs.force_build }}
34+
steps:
35+
- uses: actions/checkout@v4
36+
with:
37+
fetch-depth: 0
38+
39+
- name: Detect version changes
40+
id: detect
41+
env:
42+
BEFORE_SHA: ${{ github.event.before }}
43+
run: |
44+
python - <<'PY'
45+
import json
46+
import os
47+
import subprocess
48+
import sys
49+
import tomllib
50+
51+
event_name = os.environ.get("GITHUB_EVENT_NAME") or ""
52+
event_path = os.environ.get("GITHUB_EVENT_PATH") or ""
53+
event = None
54+
if event_path and os.path.exists(event_path):
55+
with open(event_path, "r", encoding="utf-8") as f:
56+
event = json.load(f)
57+
58+
force_build = False
59+
publish_s2and = False
60+
publish_rust = False
61+
if event_name == "workflow_dispatch":
62+
inputs = (event or {}).get("inputs", {})
63+
force_build = str(inputs.get("force_build", "")).lower() in {"1", "true", "yes", "on"}
64+
publish_s2and = str(inputs.get("publish_s2and", "")).lower() in {"1", "true", "yes", "on"}
65+
publish_rust = str(inputs.get("publish_rust", "")).lower() in {"1", "true", "yes", "on"}
66+
elif event_name == "pull_request":
67+
labels = (event or {}).get("pull_request", {}).get("labels", [])
68+
force_build = any((label or {}).get("name", "").lower() == "force-build" for label in labels)
69+
70+
before = os.environ.get("BEFORE_SHA") or ""
71+
if event_name == "pull_request" and event:
72+
before = (event.get("pull_request", {}).get("base", {}).get("sha", "")) or ""
73+
if before.startswith("0000000"):
74+
before = ""
75+
76+
def read_toml_at(path, rev=None):
77+
if rev:
78+
try:
79+
data = subprocess.check_output(["git", "show", f"{rev}:{path}"], text=True)
80+
except subprocess.CalledProcessError:
81+
return None
82+
return tomllib.loads(data)
83+
with open(path, "rb") as f:
84+
return tomllib.load(f)
85+
86+
def get_project_version(toml_obj, path):
87+
if toml_obj is None:
88+
return None
89+
try:
90+
return toml_obj["project"]["version"]
91+
except KeyError:
92+
raise SystemExit(f"Missing [project].version in {path}")
93+
94+
def get_cargo_version(toml_obj, path):
95+
if toml_obj is None:
96+
return None
97+
try:
98+
return toml_obj["package"]["version"]
99+
except KeyError:
100+
raise SystemExit(f"Missing [package].version in {path}")
101+
102+
cur_py = read_toml_at("pyproject.toml")
103+
cur_rust_py = read_toml_at("s2and_rust/pyproject.toml")
104+
cur_rust_cargo = read_toml_at("s2and_rust/Cargo.toml")
105+
106+
cur_py_ver = get_project_version(cur_py, "pyproject.toml")
107+
cur_rust_py_ver = get_project_version(cur_rust_py, "s2and_rust/pyproject.toml")
108+
cur_rust_cargo_ver = get_cargo_version(cur_rust_cargo, "s2and_rust/Cargo.toml")
109+
110+
if cur_rust_py_ver != cur_rust_cargo_ver:
111+
raise SystemExit(
112+
f"Rust version mismatch: pyproject={cur_rust_py_ver} cargo={cur_rust_cargo_ver}"
113+
)
114+
115+
before_py_ver = get_project_version(read_toml_at("pyproject.toml", before), "pyproject.toml") if before else None
116+
before_rust_py_ver = (
117+
get_project_version(read_toml_at("s2and_rust/pyproject.toml", before), "s2and_rust/pyproject.toml")
118+
if before
119+
else None
120+
)
121+
122+
s2and_changed = before_py_ver != cur_py_ver
123+
rust_changed = before_rust_py_ver != cur_rust_py_ver
124+
publish_any = s2and_changed or rust_changed or publish_s2and or publish_rust
125+
126+
out_path = os.environ["GITHUB_OUTPUT"]
127+
with open(out_path, "a", encoding="utf-8") as f:
128+
f.write(f"s2and_changed={'true' if s2and_changed else 'false'}\n")
129+
f.write(f"rust_changed={'true' if rust_changed else 'false'}\n")
130+
f.write(f"publish_any={'true' if publish_any else 'false'}\n")
131+
f.write(f"publish_s2and={'true' if publish_s2and else 'false'}\n")
132+
f.write(f"publish_rust={'true' if publish_rust else 'false'}\n")
133+
f.write(f"force_build={'true' if force_build else 'false'}\n")
134+
135+
print(f"s2and: {before_py_ver} -> {cur_py_ver} changed={s2and_changed}")
136+
print(f"rust: {before_rust_py_ver} -> {cur_rust_py_ver} changed={rust_changed}")
137+
print(f"force_build: {force_build}")
138+
print(f"publish_s2and: {publish_s2and}")
139+
print(f"publish_rust: {publish_rust}")
140+
PY
141+
142+
s2and-dist:
143+
name: s2and sdist+wheel
144+
runs-on: ubuntu-latest
145+
needs: [detect-versions]
146+
if: needs.detect-versions.outputs.s2and_changed == 'true' || needs.detect-versions.outputs.force_build == 'true' || needs.detect-versions.outputs.publish_s2and == 'true'
147+
steps:
148+
- uses: actions/checkout@v4
149+
150+
- uses: actions/setup-python@v5
151+
with:
152+
python-version: "3.11"
153+
154+
- name: Build s2and distributions
155+
run: |
156+
python -m pip install --upgrade pip
157+
python -m pip install build
158+
python -m build --sdist --wheel --outdir dist
159+
160+
- uses: actions/upload-artifact@v4
161+
with:
162+
name: dist-s2and
163+
path: dist/*
164+
165+
wheels-windows:
166+
name: wheels (windows, py${{ matrix.py }})
167+
runs-on: windows-latest
168+
needs: [detect-versions]
169+
if: needs.detect-versions.outputs.rust_changed == 'true' || needs.detect-versions.outputs.force_build == 'true' || needs.detect-versions.outputs.publish_rust == 'true'
170+
strategy:
171+
fail-fast: false
172+
matrix:
173+
py: ["3.10", "3.11", "3.12"]
174+
175+
steps:
176+
- uses: actions/checkout@v4
177+
178+
- uses: actions/setup-python@v5
179+
with:
180+
python-version: ${{ matrix.py }}
181+
182+
- name: Build wheels
183+
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380
184+
with:
185+
maturin-version: v1.11.5
186+
command: build
187+
args: --release --locked --compatibility pypi --out dist
188+
working-directory: s2and_rust
189+
190+
- uses: actions/upload-artifact@v4
191+
with:
192+
name: dist-s2and-rust-windows-py${{ matrix.py }}
193+
path: s2and_rust/dist/*
194+
195+
wheels-macos:
196+
name: wheels (macos universal2, py${{ matrix.py }})
197+
runs-on: macos-latest
198+
needs: [detect-versions]
199+
if: needs.detect-versions.outputs.rust_changed == 'true' || needs.detect-versions.outputs.force_build == 'true' || needs.detect-versions.outputs.publish_rust == 'true'
200+
strategy:
201+
fail-fast: false
202+
matrix:
203+
py: ["3.10", "3.11", "3.12"]
204+
205+
steps:
206+
- uses: actions/checkout@v4
207+
208+
- uses: actions/setup-python@v5
209+
with:
210+
python-version: ${{ matrix.py }}
211+
212+
- name: Install Rust targets (universal2)
213+
run: rustup target add x86_64-apple-darwin aarch64-apple-darwin
214+
215+
- name: Build wheels (universal2)
216+
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380
217+
env:
218+
ARCHFLAGS: "-arch x86_64 -arch arm64"
219+
with:
220+
maturin-version: v1.11.5
221+
command: build
222+
args: --release --locked --compatibility pypi --out dist
223+
working-directory: s2and_rust
224+
225+
- uses: actions/upload-artifact@v4
226+
with:
227+
name: dist-s2and-rust-macos-universal2-py${{ matrix.py }}
228+
path: s2and_rust/dist/*
229+
230+
wheels-linux:
231+
name: wheels (linux ${{ matrix.platform.name }})
232+
runs-on: ubuntu-latest
233+
needs: [detect-versions]
234+
if: needs.detect-versions.outputs.rust_changed == 'true' || needs.detect-versions.outputs.force_build == 'true' || needs.detect-versions.outputs.publish_rust == 'true'
235+
strategy:
236+
fail-fast: false
237+
matrix:
238+
platform:
239+
- name: manylinux2014-x86_64
240+
target: x86_64-unknown-linux-gnu
241+
manylinux: "2_17"
242+
# Optional: enable when you want aarch64 wheels.
243+
- name: manylinux2014-aarch64
244+
target: aarch64-unknown-linux-gnu
245+
manylinux: "2_17"
246+
- name: musllinux-x86_64
247+
target: x86_64-unknown-linux-musl
248+
manylinux: "musllinux_1_2"
249+
250+
steps:
251+
- uses: actions/checkout@v4
252+
253+
- name: Build wheels (manylinux/musllinux)
254+
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380
255+
with:
256+
maturin-version: v1.11.5
257+
command: build
258+
target: ${{ matrix.platform.target }}
259+
manylinux: ${{ matrix.platform.manylinux }}
260+
args: >
261+
--release --locked --compatibility pypi --out dist
262+
-i python3.10 -i python3.11 -i python3.12
263+
working-directory: s2and_rust
264+
265+
- uses: actions/upload-artifact@v4
266+
with:
267+
name: dist-s2and-rust-linux-${{ matrix.platform.name }}
268+
path: s2and_rust/dist/*
269+
270+
sdist:
271+
name: sdist
272+
runs-on: ubuntu-latest
273+
needs: [detect-versions]
274+
if: needs.detect-versions.outputs.rust_changed == 'true' || needs.detect-versions.outputs.force_build == 'true' || needs.detect-versions.outputs.publish_rust == 'true'
275+
steps:
276+
- uses: actions/checkout@v4
277+
278+
- name: Build sdist
279+
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380
280+
with:
281+
maturin-version: v1.11.5
282+
command: sdist
283+
args: --out dist
284+
working-directory: s2and_rust
285+
286+
- uses: actions/upload-artifact@v4
287+
with:
288+
name: dist-s2and-rust-sdist
289+
path: s2and_rust/dist/*
290+
291+
# Publish is split so skipped build jobs don't block unrelated publishes.
292+
# Manual runs can publish by setting workflow_dispatch inputs.
293+
publish-s2and:
294+
name: publish s2and to PyPI
295+
if: (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-versions.outputs.s2and_changed == 'true') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.detect-versions.outputs.publish_s2and == 'true')
296+
needs: [detect-versions, s2and-dist]
297+
runs-on: ubuntu-latest
298+
environment:
299+
name: pypi
300+
permissions:
301+
id-token: write
302+
303+
steps:
304+
- name: Download s2and dists
305+
uses: actions/download-artifact@v4
306+
with:
307+
name: dist-s2and
308+
path: dist/s2and
309+
310+
- name: Publish s2and
311+
uses: pypa/gh-action-pypi-publish@release/v1
312+
with:
313+
packages-dir: dist/s2and
314+
315+
publish-rust:
316+
name: publish s2and-rust to PyPI
317+
if: (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-versions.outputs.rust_changed == 'true') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && needs.detect-versions.outputs.publish_rust == 'true')
318+
needs: [detect-versions, wheels-windows, wheels-macos, wheels-linux, sdist]
319+
runs-on: ubuntu-latest
320+
environment:
321+
name: pypi
322+
permissions:
323+
id-token: write
324+
325+
steps:
326+
- name: Download s2and-rust dists
327+
uses: actions/download-artifact@v4
328+
with:
329+
pattern: dist-s2and-rust-*
330+
merge-multiple: true
331+
path: dist/s2and-rust
332+
333+
- name: Publish s2and-rust
334+
uses: pypa/gh-action-pypi-publish@release/v1
335+
with:
336+
packages-dir: dist/s2and-rust

0 commit comments

Comments
 (0)