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
21 changes: 21 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Open a release PR (VERSION + CHANGELOG). The version in the PR is the
# human's to edit before merging; merging tags and publishes.
name: Prepare release

on:
workflow_dispatch:
inputs:
version:
description: Release version X.Y.Z[{a|b|rc}N|.devN]; empty suggests highest tag + 1 patch
default: ""
type: string

permissions:
contents: read

jobs:
release-pr:
uses: PyDevices/.github/.github/workflows/reusable-prepare-release-pr.yml@publishing-v13
with:
version: ${{ inputs.version }}
secrets: inherit
72 changes: 72 additions & 0 deletions .github/workflows/publish-release-packages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Publish release packages

on:
release:
types:
- published
workflow_dispatch:
inputs:
release-ref:
description: Exact vX.Y.Z tag to retry
required: true
type: string

# The coordinator needs contents:write to attach release assets; a called
# workflow may not exceed the caller's grant, so the ceiling is set here.
# id-token:write is granted only to the pypi job below, the one job that
# uses it.
permissions:
contents: write

jobs:
publish:
uses: PyDevices/.github/.github/workflows/reusable-publish-release-packages.yml@publishing-v21
with:
build-kind: native-and-wasm
distribution-name: omnipython-airwindows
wheel-filename-prefix: omnipython_airwindows
# 30: cp311-cp314, cp315 and cp315t on manylinux x86_64, manylinux
# aarch64, win_amd64 and macosx_arm64 (24), cp313 and cp314 on Android
# arm64_v8a and x86_64 (4), and cp313 and cp314 on Pyodide (2).
expected-wheel-count: 30
linux-aarch64: true
# Production PyPI for final releases. The coordinator only decides;
# the upload is the pypi job below.
pypi-publish: true
release-ref: ${{ inputs.release-ref }}
secrets: inherit

pypi:
# The upload runs in this file because PyPI matches its Trusted Publisher
# against the workflow that runs the upload job. The pypi environment
# requires a reviewer's approval and allows v* tags only.
needs: publish
if: >-
!cancelled() && needs.publish.outputs.pypi-publish == 'true'
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- name: Download the validated distributions
uses: actions/download-artifact@v8
with:
name: ${{ needs.publish.outputs.dist-artifact }}
path: dist
- name: Publish to PyPI via Trusted Publishing
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true

report-pypi:
needs:
- publish
- pypi
if: >-
!cancelled() && needs.publish.outputs.pypi-publish == 'true'
uses: PyDevices/.github/.github/workflows/reusable-report-pypi-result.yml@publishing-v21
with:
distribution-name: omnipython-airwindows
version: ${{ needs.publish.outputs.version }}
pypi-result: ${{ needs.pypi.result }}
secrets: inherit
74 changes: 74 additions & 0 deletions .github/workflows/tag-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# A merged release PR (VERSION changed on main) becomes the vX.Y.Z tag and
# GitHub Release, created with the App token so publication triggers.
#
# The manual run is the recovery for a push event GitHub never delivered
# (the merge of #167, 2026-09-28: no workflow ran for it at all). It tags the
# commit on main that last changed VERSION, and refuses if that tag exists.
name: Tag release

on:
push:
branches: [main]
paths: [VERSION]
workflow_dispatch: {}

permissions:
contents: read

jobs:
tag:
if: github.event_name == 'push'
uses: PyDevices/.github/.github/workflows/reusable-tag-on-release-merge.yml@publishing-v6
secrets: inherit

tag-manual:
if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Find the release commit
id: detect
run: |
V=$(tr -d '[:space:]' < VERSION)
if ! [[ "$V" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+|\.dev[0-9]+)?$ ]]; then
echo "VERSION reads '$V', which is not a release version." >&2
exit 1
fi
if git rev-parse -q --verify "refs/tags/v$V" >/dev/null; then
echo "Tag v$V already exists; nothing to do."
echo "changed=false" >> "$GITHUB_OUTPUT"; exit 0
fi
# The merge that brought the VERSION change onto main: what the
# push run would have tagged.
SHA=$(git log -1 --first-parent --format=%H -- VERSION)
if [ "$(git show "$SHA:VERSION" | tr -d '[:space:]')" != "$V" ]; then
echo "VERSION at $SHA does not read $V." >&2
exit 1
fi
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "version=$V" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "Tagging v$V at $SHA"

- name: Mint App token
if: steps.detect.outputs.changed == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.PYDEVICES_APP_ID }}
private-key: ${{ secrets.PYDEVICES_APP_PRIVATE_KEY }}

- name: Create tag and Release
if: steps.detect.outputs.changed == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
V: ${{ steps.detect.outputs.version }}
SHA: ${{ steps.detect.outputs.sha }}
run: |
PRERELEASE=""
[[ "$V" =~ (a|b|rc)[0-9]+$|\.dev[0-9]+$ ]] && PRERELEASE="--prerelease"
gh release create "v$V" --target "$SHA" \
--title "v$V" --generate-notes $PRERELEASE
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,46 @@ The tests are:
Everything the builds fetch or test against is pinned in
[`DEPENDENCIES.lock`](DEPENDENCIES.lock), and CI reads its pins from there.

## Build and release wheels

`omnipython-airwindows` is a native CPython distribution. `python -m build`
makes a local source archive and wheel.

A GitHub Release `vX.Y.Z`, matching the `VERSION` file (which is also where
`audioairwindows.__version__` comes from), starts the organization's reusable
publishing workflow. The pin is the `@publishing-vN` ref in
`.github/workflows/publish-release-packages.yml`, with
`build-kind: native-and-wasm`. It builds and validates exactly the number of
wheels `expected-wheel-count` in that file names, and the settings that choose
them are in `[tool.cibuildwheel]` in `pyproject.toml`. If the count changes,
change it in the same pull request. A release builds 30 wheels:

- CPython 3.11 to 3.15, plus the free-threaded 3.15t, on manylinux_2_28
x86_64, manylinux_2_28 aarch64, Windows AMD64 and macosx_arm64: 24.
- CPython 3.13 to 3.14, Android API 21 arm64_v8a and x86_64: 4.
- CPython 3.13 to 3.14, Pyodide wasm32: 2.

Every wheel compiles all 530 plug-ins, so a release build takes much longer
than a test run. Each wheel except the 3.15 ones, Android and Pyodide
installs into a clean environment and runs the simple test.

To release, run the "Prepare release" workflow. It opens a pull request with
`VERSION` and the changelog; merging it makes the tag and the GitHub Release,
and the Release starts "Publish release packages". Every release goes to
TestPyPI and is attached to its GitHub Release. A pre-release
(`X.Y.ZrcN`, `X.Y.Z.devN`) stops there. A final release then goes to
production PyPI after a maintainer approves the `pypi` environment; that
upload is the `pypi` job in `publish-release-packages.yml`, not in the
reusable workflow, because PyPI's Trusted Publishing only matches the
workflow that runs the upload.

If a publish run is interrupted, run it again for the same tag:

```sh
gh workflow run publish-release-packages.yml --repo PyDevices/airwindows \
-f release-ref=vX.Y.Z
```

## Build the unix MicroPython the tests use

Follow [Install](docs/get-started/install.md#micropython) with the unix
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ audiodsp's audio graph on CPython and MicroPython-compatible firmware.

## Install

On CPython, beside [pydevices-audiodsp](https://pypi.org/project/pydevices-audiodsp/) 0.6.10 or later:
On CPython 3.11 or later:

```sh
pip install git+https://github.com/PyDevices/airwindows
pip install omnipython-airwindows
```

That installs a prebuilt wheel, and [pydevices-audiodsp](https://pypi.org/project/pydevices-audiodsp/) 0.6.10 or later beside it. [Install](docs/get-started/install.md) lists the platforms that have a wheel and how to build from source on the rest.

On MicroPython, build this repository into the firmware next to audiodsp
0.6.10 or later: [Install](docs/get-started/install.md) has the command.

Expand Down
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
no double in their FPUs either. Measure each with the fast path on, and
turn it on where it helps.
- **A PyPI release**, so `pip install omnipython-airwindows` works without git, with
prebuilt wheels so the install doesn't compile every plug-in.
prebuilt wheels so the install doesn't compile every plug-in. The wheels
build in CI; the first release is what's left.
43 changes: 39 additions & 4 deletions docs/get-started/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,47 @@ The effects are audiodsp audio nodes, so audiodsp comes first: version
## CPython

```sh
pip install git+https://github.com/PyDevices/airwindows
pip install omnipython-airwindows
```

That installs [pydevices-audiodsp](https://pypi.org/project/pydevices-audiodsp/)
too, and builds a C extension with every plug-in in it, so you need a C
compiler and a few minutes. Python 3.11 or later.
That installs a prebuilt wheel with every plug-in in it, so nothing
compiles, and [pydevices-audiodsp](https://pypi.org/project/pydevices-audiodsp/)
beside it. Python 3.11 or later.

A wheel is published for:

- Linux, x86_64 and aarch64 (64-bit Raspberry Pi OS), glibc 2.28 or later
- Windows, 64-bit
- macOS on Apple Silicon
- Android, arm64 and x86_64, and Pyodide, on Python 3.13 and 3.14

Python 3.15 and its free-threaded build have wheels on Linux, Windows and
macOS. A pre-release (`.devN`, `rcN`) is published to TestPyPI first, and
`pip` skips pre-releases unless you ask for one:

```sh
pip install --pre -i https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ omnipython-airwindows
```

With `uv`, add `--index-strategy unsafe-best-match`, or it resolves the
name on PyPI before TestPyPI and installs the wrong version:

```sh
uv pip install --prerelease allow --index-strategy unsafe-best-match \
-i https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ omnipython-airwindows
```

### Build from source

Anywhere else (Alpine Linux, Intel Macs, 32-bit Python), `pip` has no
wheel for you, so build the extension yourself. It needs a C compiler and a
few minutes, because it compiles about 530 plug-ins:

```sh
pip install git+https://github.com/PyDevices/airwindows
```

## MicroPython

Expand Down
42 changes: 42 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,45 @@ py-modules = ["audioairwindows"]

[tool.setuptools.dynamic]
version = {file = "VERSION"}

[tool.cibuildwheel]
build = "cp311-* cp312-* cp313-* cp314-* cp315-* cp315t-*"
# 3.15 and its free-threaded build on the desktops only: Android and Pyodide
# have no 3.15 users yet.
skip = "*musllinux* *-win32 cp315*-android_* cp315*-pyodide_*"
# Each wheel installs into a clean environment with audiodsp beside it and
# renders PurestDrive. pydevices-audiodsp's PyPI releases have no Linux aarch64
# wheel and no 3.15 wheel, so the test environment takes omnipython-audiodsp,
# the same code under its current name, from TestPyPI and installs the wheel
# without resolving its dependencies.
test-command = "python {project}/examples/audioairwindows_simpletest.py"
test-skip = "*-pyodide_* *-android_*"
before-test = "pip install --pre -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ omnipython-audiodsp"
test-environment = { PIP_NO_DEPS = "1" }

# auto64 is the runner's own 64-bit architecture: x86_64 on an x86_64 runner,
# aarch64 on an ARM one, so one setting serves both.
[tool.cibuildwheel.linux]
archs = ["auto64"]
manylinux-x86_64-image = "manylinux_2_28"
manylinux-aarch64-image = "manylinux_2_28"
# No debug information: it made these wheels six times the size of the others.
# The flag comes after the interpreter's own -g, so it wins; optimisation is
# untouched.
environment = { CFLAGS = "-g0" }

# arm64 only, matching the runner: an x86_64 wheel built there couldn't run
# its test.
[tool.cibuildwheel.macos]
archs = ["arm64"]

[tool.cibuildwheel.windows]
archs = ["AMD64"]

[tool.cibuildwheel.android]
archs = ["arm64_v8a", "x86_64"]
environment = { ANDROID_API_LEVEL = "21" }

[[tool.cibuildwheel.overrides]]
select = "*-android_*"
build-frontend = "build"
Loading