Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash

eval "$(devenv direnvrc)"

# You can pass flags to the devenv command
# For example: use devenv --impure --option services.postgres.enable:bool true
use devenv
60 changes: 17 additions & 43 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,52 @@ concurrency:

jobs:
# ============================================================================
# Static checks: lint + format. Runs first so style violations surface fast.
# Does NOT require building the Rust extension or installing runtime deps,
# so it stays cheap and quick.
# Static checks: lint + format.
# Resolve the project with uv, then execute tools from the synced env.
# ============================================================================
lint:
name: Ruff (lint + format)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"

- name: Install ruff
run: pip install "ruff>=0.6"
- name: Sync dev environment
run: uv sync --dev --frozen

- name: ruff check
run: ruff check canopus
run: uv run ruff check

- name: ruff format --check
run: ruff format --check canopus
run: uv run ruff format --check

# ============================================================================
# Build the Rust extension and run the test suite across the matrix.
# Each cell needs a Rust toolchain + the Python deps from pyproject.toml.
#
# NOTE: the `monodromy` package is not on PyPI and is required by parts of
# canopus at import time. Until monodromy lands on PyPI (or canopus moves
# to lazy imports), the test job is allowed to fail gracefully via
# `continue-on-error` on monodromy-dependent paths.
# Each cell resolves the locked project env via uv, then runs commands
# through uv so the compiled extension and Python tooling come from one place.
# ============================================================================
test:
name: Test (${{ matrix.os }}, py${{ matrix.python }})
needs: lint
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
- uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python }}
enable-cache: true

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Install lrslib (Linux only — enables monodromy-backed tests)
# `lrs` is in apt on Debian/Ubuntu but has no homebrew formula, so we
# install it only on Linux. macOS runners will skip monodromy-dependent
# tests via the `requires_lrs` pytest marker.
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y lrslib

- name: Cache cargo build
uses: actions/cache@v4
with:
Expand All @@ -82,25 +70,11 @@ jobs:
restore-keys: |
cargo-${{ runner.os }}-${{ matrix.python }}-

- name: Build & install canopus with test extras (editable)
# Editable install via PEP 660: pip → maturin places the compiled `_accel`
# extension inside the source tree, so subsequent `import canopus` resolves
# to this tree (which Python prefers because cwd is on sys.path[0]) and
# finds the .so. Plain `pip install .` would put it in site-packages but
# `import canopus` from the repo root still picks the source tree first,
# which then has no .so → ModuleNotFoundError.
run: |
pip install --upgrade pip
pip install -e ".[test]"

- name: Smoke test (import canopus without monodromy)
# Verifies the wheel was built correctly and the lazy-import refactor
# truly lets `import canopus` succeed in a monodromy-free environment.
run: python -c "import canopus; print('canopus imports OK from', canopus.__file__)"
- name: Sync dev environment
run: uv sync --dev --frozen

- name: Install monodromy (optional, may fail)
continue-on-error: true
run: pip install "git+https://github.com/Youngcius/monodromy"
- name: Smoke test (import canopus)
run: uv run python -c "import canopus; print('canopus imports OK from', canopus.__file__)"

- name: Run pytest
run: pytest -ra
run: uv run pytest -ra
77 changes: 15 additions & 62 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ name: Release
# Intel Macs have been declining fast and the macos-13 runner queue is
# slow; Intel Mac users will install from sdist, which compiles locally.)
# 2. Builds a source distribution (sdist).
# 3. Publishes every artifact to PyPI via OIDC trusted publishing.
# 3. Tests the built wheels out-of-tree to ensure functionality.
# 4. Publishes every artifact to PyPI via OIDC trusted publishing.
#
# === Prerequisites (do this once, BEFORE pushing a tag) ===
# 1. Reserve the project on PyPI:
Expand All @@ -31,83 +32,40 @@ on:
permissions:
contents: read

# Cancel an in-flight release on the same tag if a new push lands.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
# ============================================================================
# Build native wheels via maturin across the supported OS × arch matrix.
# Each cell builds wheels for CPython 3.10 – 3.13 in one shot using maturin's
# multi-interpreter mode.
# ============================================================================
build-wheels:
name: Wheels (${{ matrix.target }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: linux-x86_64
runner: ubuntu-latest
maturin-target: x86_64-unknown-linux-gnu
manylinux: auto
- target: macos-arm64
runner: macos-14
maturin-target: aarch64-apple-darwin
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: |
3.10
3.11
3.12
3.13

- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.maturin-target }}
manylinux: ${{ matrix.manylinux || '' }}
args: --release --strip --out dist --interpreter python3.10 python3.11 python3.12 python3.13

- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist/*.whl
wheels:
uses: ./.github/workflows/reusable-wheels.yml

# ============================================================================
# Source distribution. Built once, on Linux.
# 构建 Sdist
# ============================================================================
build-sdist:
name: Source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build sdist
uses: PyO3/maturin-action@v1
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
command: sdist
args: --out dist

enable-cache: true
- name: Build sdist
run: uv build --sdist --out-dir dist
- name: Upload sdist artifact
uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz

# ============================================================================
# Publish to PyPI via OIDC trusted publishing.
# Requires the GitHub Environment `pypi` (no token secret needed).
# 发布到 PyPI (必须等待 build-wheels, build-sdist, 以及 test-wheels 成功)
# ============================================================================
publish:
name: Publish to PyPI
needs: [build-wheels, build-sdist]
needs: [wheels, build-sdist]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment:
Expand All @@ -121,14 +79,9 @@ jobs:
with:
path: dist
merge-multiple: true

- name: List artifacts
run: ls -lh dist/

- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
# skip-existing prevents the workflow from failing if a wheel was
# already uploaded by a previous run on the same tag.
skip-existing: true
run: uv publish dist/* --skip-existing
115 changes: 115 additions & 0 deletions .github/workflows/reusable-wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Reusable Wheels

on:
workflow_call:

permissions:
contents: read

jobs:
# ============================================================================
# 构建 Wheels
# ============================================================================
build-wheels:
name: Wheels (${{ matrix.target }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: linux-x86_64
runner: ubuntu-latest
maturin-target: x86_64-unknown-linux-gnu
manylinux: auto
- target: macos-arm64
runner: macos-14
maturin-target: aarch64-apple-darwin
- target: windows-x86_64
runner: windows-latest
maturin-target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: |
3.10
3.11
3.12
3.13
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.maturin-target }}
manylinux: ${{ matrix.manylinux || '' }}
args: --release --strip --out dist --interpreter 3.10 3.11 3.12 3.13
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: dist/*.whl

# ============================================================================
# 树外测试 Wheel (Out-of-tree Test)
# ============================================================================
test-wheels:
name: Test Wheels (${{ matrix.target }}, Python ${{ matrix.python-version }})
needs: build-wheels
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
target: [linux-x86_64, macos-arm64, windows-x86_64]
python-version: ["3.10", "3.11", "3.12", "3.13"]
include:
- target: linux-x86_64
runner: ubuntu-latest
- target: macos-arm64
runner: macos-14
- target: windows-x86_64
runner: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Download wheel artifact
uses: actions/download-artifact@v4
with:
name: wheels-${{ matrix.target }}
path: ${{ runner.temp }}/wheel-dist
- name: Install and test wheel in an isolated project
shell: bash
run: |
set -euo pipefail

PY_VER_NODOT=$(echo "${{ matrix.python-version }}" | tr -d '.')
WHEEL_DIR="${{ runner.temp }}/wheel-dist"
WHEEL=$(find "$WHEEL_DIR" -name "*cp${PY_VER_NODOT}*.whl" -print -quit)

if [[ -z "$WHEEL" ]]; then
WHEEL=$(find "$WHEEL_DIR" -name '*.whl' -print -quit)
fi

if [[ -z "$WHEEL" ]]; then
echo "No wheel artifact found for Python ${{ matrix.python-version }}!"
exit 1
fi
echo "Found wheel: $WHEEL"

TEST_PROJECT="${{ runner.temp }}/test-project"
python -m venv "$TEST_PROJECT/.venv"

if [[ "$RUNNER_OS" == "Windows" ]]; then
PYTHON_BIN="$TEST_PROJECT/.venv/Scripts/python.exe"
else
PYTHON_BIN="$TEST_PROJECT/.venv/bin/python"
fi

"$PYTHON_BIN" -m pip install --upgrade pip
"$PYTHON_BIN" -m pip install "$WHEEL" pytest pytest-cov

cd "$TEST_PROJECT"

"$PYTHON_BIN" -m pytest "$GITHUB_WORKSPACE/tests" --rootdir "$TEST_PROJECT" --import-mode=importlib
22 changes: 22 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Wheels

# Triggered on branch pushes / PRs to validate distributable wheels before a
# tagged release. This workflow builds platform wheels and runs out-of-tree
# tests against the built artifacts, but never uploads anything to PyPI.

on:
push:
branches: [master, main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: wheels-${{ github.ref }}
cancel-in-progress: true

jobs:
wheels:
uses: ./.github/workflows/reusable-wheels.yml
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,10 @@ output/
results/

.claude
.devenv
.devenv.flake.nix


# Added by cargo

/target
Loading
Loading