From 1780f52349fc2bbd9282801f96e89d7b2e0c8904 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 01:44:14 -0600 Subject: [PATCH 1/7] Release chain: prebuilt wheels and the publish workflows (wip) --- .github/workflows/prepare-release.yml | 21 ++++++ .../workflows/publish-release-packages.yml | 72 ++++++++++++++++++ .github/workflows/tag-release.yml | 74 +++++++++++++++++++ .github/workflows/wheel-proof.yml | 17 +++++ pyproject.toml | 34 +++++++++ 5 files changed, 218 insertions(+) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/publish-release-packages.yml create mode 100644 .github/workflows/tag-release.yml create mode 100644 .github/workflows/wheel-proof.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..36126aa --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -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 diff --git a/.github/workflows/publish-release-packages.yml b/.github/workflows/publish-release-packages.yml new file mode 100644 index 0000000..80cd775 --- /dev/null +++ b/.github/workflows/publish-release-packages.yml @@ -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 diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml new file mode 100644 index 0000000..59ecfde --- /dev/null +++ b/.github/workflows/tag-release.yml @@ -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 diff --git a/.github/workflows/wheel-proof.yml b/.github/workflows/wheel-proof.yml new file mode 100644 index 0000000..b314796 --- /dev/null +++ b/.github/workflows/wheel-proof.yml @@ -0,0 +1,17 @@ +name: wheel proof (temporary) +on: + pull_request: + paths: ['.github/workflows/wheel-proof.yml', 'pyproject.toml'] + workflow_dispatch: +permissions: + contents: read +jobs: + build: + uses: PyDevices/.github/.github/workflows/reusable-build-native-and-wasm-wheels.yml@publishing-v21 + with: + distribution-name: omnipython-airwindows + wheel-filename-prefix: omnipython_airwindows + expected-wheel-count: 30 + linux-aarch64: true + release-ref: ${{ github.head_ref || github.ref_name }} + version: "0.1.0" diff --git a/pyproject.toml b/pyproject.toml index 88edf41..1276762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,3 +29,37 @@ 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. audiodsp's wheels for 3.15 aren't on PyPI yet, so those +# two builds are compiled and packaged but not run. +test-command = "python {project}/examples/audioairwindows_simpletest.py" +test-skip = "cp315-* cp315t-* *-pyodide_* *-android_*" + +# 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" + +# 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" From 8ce4b72d41a5faf4fb43f0830784fa8b31a8e0c8 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 01:45:15 -0600 Subject: [PATCH 2/7] Install: pip install omnipython-airwindows, the wheels and the release steps --- CONTRIBUTING.md | 40 ++++++++++++++++++++++++++++++++++ README.md | 6 ++++-- ROADMAP.md | 3 ++- docs/get-started/install.md | 43 +++++++++++++++++++++++++++++++++---- 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eee6a37..b460110 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index f817a07..ab55331 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/ROADMAP.md b/ROADMAP.md index 414bad4..5e95d8c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. diff --git a/docs/get-started/install.md b/docs/get-started/install.md index e05c062..4111c2f 100644 --- a/docs/get-started/install.md +++ b/docs/get-started/install.md @@ -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 From 5858d5bfad2046e4ed79b191e43be319644e2826 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 01:54:31 -0600 Subject: [PATCH 3/7] Wheels: test each one against omnipython-audiodsp from TestPyPI --- pyproject.toml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1276762..4596f3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,14 @@ build = "cp311-* cp312-* cp313-* cp314-* cp315-* cp315t-*" # 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. audiodsp's wheels for 3.15 aren't on PyPI yet, so those -# two builds are compiled and packaged but not run. +# 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 = "cp315-* cp315t-* *-pyodide_* *-android_*" +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. From 4133d04d3302d03a13fc1aed04ca59d107951ca1 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 02:38:59 -0600 Subject: [PATCH 4/7] Release chain: drop the build-only proof workflow --- .github/workflows/wheel-proof.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/wheel-proof.yml diff --git a/.github/workflows/wheel-proof.yml b/.github/workflows/wheel-proof.yml deleted file mode 100644 index b314796..0000000 --- a/.github/workflows/wheel-proof.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: wheel proof (temporary) -on: - pull_request: - paths: ['.github/workflows/wheel-proof.yml', 'pyproject.toml'] - workflow_dispatch: -permissions: - contents: read -jobs: - build: - uses: PyDevices/.github/.github/workflows/reusable-build-native-and-wasm-wheels.yml@publishing-v21 - with: - distribution-name: omnipython-airwindows - wheel-filename-prefix: omnipython_airwindows - expected-wheel-count: 30 - linux-aarch64: true - release-ref: ${{ github.head_ref || github.ref_name }} - version: "0.1.0" From dd11fddf065f346d6147fbd786e1bd7debec7d07 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 03:09:29 -0600 Subject: [PATCH 5/7] Wheels: build the Linux extension without debug information --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4596f3c..7e35e41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,10 @@ test-environment = { PIP_NO_DEPS = "1" } 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. From 94123fb9f3f55f6c6719b38bc579689dad0cc096 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 03:09:29 -0600 Subject: [PATCH 6/7] Release chain: build-only proof workflow (temporary) --- .github/workflows/wheel-proof.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/wheel-proof.yml diff --git a/.github/workflows/wheel-proof.yml b/.github/workflows/wheel-proof.yml new file mode 100644 index 0000000..b314796 --- /dev/null +++ b/.github/workflows/wheel-proof.yml @@ -0,0 +1,17 @@ +name: wheel proof (temporary) +on: + pull_request: + paths: ['.github/workflows/wheel-proof.yml', 'pyproject.toml'] + workflow_dispatch: +permissions: + contents: read +jobs: + build: + uses: PyDevices/.github/.github/workflows/reusable-build-native-and-wasm-wheels.yml@publishing-v21 + with: + distribution-name: omnipython-airwindows + wheel-filename-prefix: omnipython_airwindows + expected-wheel-count: 30 + linux-aarch64: true + release-ref: ${{ github.head_ref || github.ref_name }} + version: "0.1.0" From 28bae00ffb19ef455cc3be7542ca41ed81a0c061 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 10 Nov 2026 03:30:42 -0600 Subject: [PATCH 7/7] Release chain: drop the build-only proof workflow --- .github/workflows/wheel-proof.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/wheel-proof.yml diff --git a/.github/workflows/wheel-proof.yml b/.github/workflows/wheel-proof.yml deleted file mode 100644 index b314796..0000000 --- a/.github/workflows/wheel-proof.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: wheel proof (temporary) -on: - pull_request: - paths: ['.github/workflows/wheel-proof.yml', 'pyproject.toml'] - workflow_dispatch: -permissions: - contents: read -jobs: - build: - uses: PyDevices/.github/.github/workflows/reusable-build-native-and-wasm-wheels.yml@publishing-v21 - with: - distribution-name: omnipython-airwindows - wheel-filename-prefix: omnipython_airwindows - expected-wheel-count: 30 - linux-aarch64: true - release-ref: ${{ github.head_ref || github.ref_name }} - version: "0.1.0"