jlevy / jlevy/flowmark-rs

Distribute flowmark-rs on PyPI via maturin (bindings = bin)

Open
#36 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
17
Forks
2
Avg merge
5h 10m
Merged PRs (30d)
7

Description

## Goal

Publish flowmark-rs to PyPI as a pre-built binary so users can install and run it with:

```bash
uvx flowmark-rs # run on demand (like npx)
uv tool install flowmark-rs # persistent install
pip install flowmark-rs # classic pip
```

No Rust toolchain required for end users. Just `uv` or `pip`.

## Why this approach

The original flowmark is a Python package on PyPI. The Rust rewrite is dramatically faster. Distributing the Rust binary through the same PyPI ecosystem keeps the install experience identical — just a different package name.

This is the same pattern used by **ruff**, **uv**, and **maturin** itself: a pure Rust CLI binary, packaged as a Python wheel, published to PyPI.

## How it works

[Maturin](https://github.com/PyO3/maturin) builds platform-specific Python wheels that contain the compiled Rust binary. When installed, the binary is placed on the user's PATH (e.g., in a virtualenv's `bin/` directory). No Python code runs — it's just a distribution mechanism.

The key setting is `bindings = "bin"` in `pyproject.toml`, which tells maturin this is a standalone binary (not a Python extension module).

## Files to add

### 1. `pyproject.toml`

Add at the repo root, next to `Cargo.toml`:

```toml
[build-system]
requires = ["maturin>=1.9,<2.0"]
build-backend = "maturin"

[project]
name = "flowmark-rs"
description = "Fast markdown formatter, written in Rust"
requires-python = ">=3.8"
license = { text = "MIT" }
readme = "README.md"
dynamic = ["version"]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Programming Language :: Rust",
"Topic :: Text Processing :: Markup :: Markdown",
]

[project.urls]
Repository = "https://github.com/jlevy/flowmark-rs"

[tool.maturin]
bindings = "bin"
strip = true
```

Key settings:

- **`bindings = "bin"`** — CLI binary, not a Python extension
- **`strip = true`** — strip debug symbols to reduce binary size (ruff does this)
- **`dynamic = ["version"]`** — maturin reads the version from `Cargo.toml` automatically

### 2. `.github/workflows/release.yml`

Builds wheels for all major platforms and publishes to PyPI on tagged releases:

```yaml
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: read

jobs:
# ── Build: Linux x86_64 ──────────────────────────────────────────
build-linux-x86_64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
maturin-version: latest
command: build
args: --release --locked --out dist
target: x86_64-unknown-linux-gnu
manylinux: "2_17"
- uses: actions/upload-artifact@v4
with:
name: wheels-linux-x86_64
path: dist/*.whl

# ── Build: Linux aarch64 ─────────────────────────────────────────
build-linux-aarch64:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
maturin-version: latest
command: build
args: --release --locked --out dist
target: aarch64-unknown-linux-gnu
manylinux: "2_17"
- uses: actions/upload-artifact@v4
with:
name: wheels-linux-aarch64
path: dist/*.whl

# ── Build: macOS x86_64 ──────────────────────────────────────────
build-macos-x86_64:
runs-on: macos-13
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
maturin-version: latest
command: build
args: --release --locked --out dist
target: x86_64-apple-darwin
- uses: actions/upload-artifact@v4
with:
name: wheels-macos-x86_64
path: dist/*.whl

# ── Build: macOS aarch64 (Apple Silicon) ──────────────────────────
build-macos-aarch64:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
maturin-version: latest
command: build
args: --release --locked --out dist
target: aarch64-apple-darwin
- uses: actions/upload-artifact@v4
with:
name: wheels-macos-aarch64
path: dist/*.whl

# ── Build: Windows x86_64 ────────────────────────────────────────
build-windows-x86_64:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
maturin-version: latest
command: build
args: --release --locked --out dist
target: x86_64-pc-windows-msvc
- uses: actions/upload-artifact@v4
with:
name: wheels-windows-x86_64
path: dist/*.whl

# ── Build: source distribution ───────────────────────────────────
build-sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
maturin-version: latest
command: sdist
args: --out dist
- uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist/*.tar.gz

# ── Publish to PyPI ──────────────────────────────────────────────
publish:
needs:
- build-linux-x86_64
- build-linux-aarch64
- build-macos-x86_64
- build-macos-aarch64
- build-windows-x86_64
- build-sdist
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # Required for PyPI trusted publishing
steps:
- uses: astral-sh/setup-uv@v7
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
merge-multiple: true
path: wheels/
- run: uv publish wheels/*
```

## Notes on the workflow

- **manylinux: "2_17"** — glibc 2.17 compatibility covers virtually all Linux distros still in use (CentOS 7+, Ubuntu 14.04+, Debian 8+)
- **PyPI trusted publishing** — uses OIDC (`id-token: write`) so no API token secret is needed. Configure in PyPI project settings under "Trusted Publishers"
- **`--locked`** — uses `Cargo.lock` for reproducible builds
- **macOS runners** — `macos-13` is x86_64, `macos-14` is aarch64 (Apple Silicon)
- **sdist** — source distribution allows users to build from source as a fallback

## Platform coverage

| Platform | Target | Runner |
| --- | --- | --- |
| Linux x86_64 | `x86_64-unknown-linux-gnu` | `ubuntu-latest` |
| Linux ARM64 | `aarch64-unknown-linux-gnu` | `ubuntu-latest` (cross-compiled) |
| macOS Intel | `x86_64-apple-darwin` | `macos-13` |
| macOS Apple Silicon | `aarch64-apple-darwin` | `macos-14` |
| Windows x86_64 | `x86_64-pc-windows-msvc` | `windows-latest` |

Additional targets (musl, armv7, Windows ARM) can be added later — see [ruff's build-binaries.yml](https://github.com/astral-sh/ruff/blob/main/.github/workflows/build-binaries.yml) for a comprehensive 18-target example.

## Local development

```bash
# Install maturin
uv tool install maturin

# Build locally
maturin build --release

# Build + install into current venv
maturin develop --release

# Test the installed binary
flowmark-rs --help
```

## PyPI setup checklist

1. **Create the PyPI project** — register `flowmark-rs` on https://pypi.org
2. **Configure trusted publishing** — in PyPI project settings, add a trusted publisher for the GitHub repo + workflow file + environment name (`release`)
3. **Tag a release** — `git tag v0.1.0 && git push --tags` triggers the workflow
4. **Verify** — `uvx flowmark-rs --version` should work within minutes of publish

## Consumer migration

Once published, downstream projects switch with a one-line change:

```diff
- uvx flowmark
+ uvx flowmark-rs
```

Same UX. Same `uvx` ecosystem. Much faster.

## References

- [Maturin user guide](https://www.maturin.rs/) — full documentation
- [Maturin `bin` bindings](https://www.maturin.rs/bindings) — binary-only distribution
- [PyO3/maturin-action](https://github.com/PyO3/maturin-action) — GitHub Actions for cross-platform builds
- [Ruff's pyproject.toml](https://github.com/astral-sh/ruff/blob/main/pyproject.toml) — real-world reference (same pattern)
- [Ruff's build-binaries.yml](https://github.com/astral-sh/ruff/blob/main/.github/workflows/build-binaries.yml) — comprehensive 18-target build matrix
- [Maturin distribution guide](https://www.maturin.rs/distribution.html) — manylinux, cross-compilation, zig

Contributor guide

Open the contributing guide

Research direction

Start by inspecting Cargo.toml and running the documented maturin build --release command locally. Add the specified pyproject.toml and .github/workflows/release.yml, then verify that the workflow builds the listed platform wheels and source distribution. Done means the tagged-release workflow publishes successfully and uvx flowmark-rs --version works from PyPI.

Written by the indexing model from the issue text.

Assessment

Tech stack
github-actions, python, rust
Domain
build-system, ci-cd, cli, release
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.