-
Notifications
You must be signed in to change notification settings - Fork 1
357 lines (317 loc) · 16.2 KB
/
Copy pathpython-wheel.yml
File metadata and controls
357 lines (317 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
name: Python Wheel
on:
push:
branches: [main]
# Prose cannot break a wheel build or a type check, and this is the slowest
# job in the repo. `main` carries no required status checks, so a skipped run
# blocks nothing. `.github/**` is deliberately absent: a workflow edit must
# run the workflow.
paths-ignore:
- 'docs/**'
- '**.md'
- '.claude/**'
- 'LICENSE'
- 'LICENSES/**'
pull_request:
branches: [main]
paths-ignore:
- 'docs/**'
- '**.md'
- '.claude/**'
- 'LICENSE'
- 'LICENSES/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs:
wheel-interpreter-lifecycle:
name: Wheel build + interpreter lifecycle (no GPU)
runs-on: ubuntu-latest
# A lock-order defect between the lifecycle mutex and the GIL wedges the
# whole interpreter rather than raising, so its regression test can only go
# red by running out of time. Without a bound it would burn the default six
# hours instead.
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
# Compiles and runs PR-authored code; a persisted checkout token
# would be readable by it. No workspace member has a git dependency.
persist-credentials: false
# Same minimal engine build set as test.yml: the engine's build.rs
# compiles GLSL with glslc, the Linux dep set links Vulkan, and iceoryx2's
# PAL crates run bindgen. `python3-dev` is the extra — the wheel crate's
# unit tests link libpython (see the step below).
# `cmake` builds the engine's vendored GLSL compiler (shaderc, taken
# `build-from-source`); `ninja-build` is what makes that ~64s instead of
# minutes — shaderc-sys picks the Ninja generator whenever `ninja` is on
# PATH, so installing it is the whole configuration. Explicit rather than
# leaning on the runner image having either, whose contents move.
- name: Install system dependencies
timeout-minutes: 12
uses: ./.github/actions/install-linux-engine-build-dependencies
with:
packages: >-
pkg-config
protobuf-compiler
libclang1-18
libclang-common-18-dev
libvulkan-dev
glslc
python3-dev
cmake
ninja-build
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# Was a raw `actions/cache` over `target/`. Three near-identical 1.97 GB
# entries existed at once — one for main and one for each open PR — because
# Actions caches are ref-scoped and a PR's save is unreadable by any other
# PR. `save-if` on main means PRs restore and save nothing.
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: wheel
save-if: ${{ github.ref == 'refs/heads/main' }}
# Scoped to the signals module rather than the whole engine lib suite,
# which is a tracked follow-up (see test.yml) pending a parallel-run
# flake. These need no GPU, and without them the ownership, restoration,
# retake and stale-signal locks run nowhere in CI.
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Build and install the wheel
# The local build. Release packaging — repo release artifacts and the
# PEP 503 simple index — is `release-wheel.yml`.
working-directory: sdk/streamlib-python-wheel
run: |
uv venv --python 3.12 .venv
# numpy rides along because the exchange-surface tests import it at
# module scope — collection needs it even when `requires_gpu`
# deselects every test that would use it. pydantic is what proves a
# model is a legal `read(port, into=T)` target; the wheel never
# imports it.
VIRTUAL_ENV="$PWD/.venv" uv pip install pytest mypy numpy pydantic typing_extensions
VIRTUAL_ENV="$PWD/.venv" uvx maturin@1.9.6 develop
# An extension wheel depends on `streamlib` as a binary, so its lane
# installs this artifact rather than building the engine a second time —
# which is the slowest job in the repo and would be duplicated per
# extension. Incremental over the develop build above, so it costs
# seconds rather than minutes.
- name: Package the wheel for the extension lane
working-directory: sdk/streamlib-python-wheel
run: VIRTUAL_ENV="$PWD/.venv" uvx maturin@1.9.6 build --out dist
- uses: actions/upload-artifact@v4
with:
name: streamlib-wheel
path: sdk/streamlib-python-wheel/dist/*.whl
retention-days: 1
if-no-files-found: error
# The crate's own unit tests link libpython rather than building as an
# extension module (the `extension-module` feature is off by default for
# exactly this), which is why `python3-dev` is in the apt list above.
# Without this step they run on developer machines only.
# `--test-threads=1` because these tests are not independent: every one of
# them embeds the *same* CPython interpreter and takes the *same* GIL, so
# the harness's default parallelism has them racing inside one runtime.
# What that surfaces as is an import race — a thread reaching
# `typing.ClassVar` while another is still importing `typing` fails with
# "partially initialized module", which is what turned `main` red on
# e2429dcd after nine green runs. Serial is the honest shape for an
# embedded interpreter and costs nothing measurable: the whole suite is
# ~2s either way.
- name: Wheel crate unit tests
run: cargo test --locked -p streamlib-python-wheel --lib -- --test-threads=1
# The tap controls decode transport-framed bags through the wheel's own
# decoder, so unlike the loopback fixture's analysis half they cannot ride
# the no-build job — they need the wheel this job installed above.
- name: Audio channel tap controls
working-directory: runtime/streamlib-engine/tests/fixtures
run: >-
"$GITHUB_WORKSPACE/sdk/streamlib-python-wheel/.venv/bin/python"
-m unittest test_tap_audio_channel
# The only check that compares the stub to the binary. It imports the
# built module, introspects it, and fails on a name or signature the
# hand-written `_engine.pyi` no longer describes — which is what makes a
# hand-maintained stub safe. `pyright --verifytypes` cannot substitute:
# it scores annotation completeness, so a stub describing a method the
# binary dropped still reads as 100% complete.
- name: Type stubs match the compiled module
working-directory: sdk/streamlib-python-wheel
run: .venv/bin/python -m mypy.stubtest streamlib._engine
# The runner has no GPU. `Runner::start()` initializes a GPU context whose
# DMA-BUF pool pre-warm needs a driver that can allocate exportable device
# memory, and a software rasterizer (lavapipe) cannot — verified in a
# GPU-less container. So this job proves the half of the contract that
# does not need a device: the wheel imports, the engine boots and tears
# down, and neither the exception path nor a Runtime still referenced at
# interpreter exit hangs finalization. The `requires_gpu` half — Ctrl-C
# during and after startup, GIL release while blocking, SIGINT handback,
# cross-thread shutdown, no surviving process group — is rig-only by
# owner decision (2026-08-03) rather than a self-hosted GPU runner.
- name: Interpreter-lifecycle contract (GPU-free half)
working-directory: sdk/streamlib-python-wheel
run: |
.venv/bin/python -m pytest tests/ -v -m "not requires_gpu"
# The Rust half of the same lifecycle contract the pytest step above
# checks from Python: who owns SIGINT while `rt.run()` blocks, and that a
# second stop is a no-op. `test.yml` deliberately does not run
# streamlib-engine's lib tests, so this carve-out is their only home.
#
# Last, so that a failure here cannot cost the five steps above their
# report — while this ran first, the wheel was never built when it failed.
- name: Shutdown-signal ownership and stop-idempotency tests
run: |
cargo test --locked -p streamlib-engine --lib -- \
core::signals core::runtime::runtime::tests::stopping
extension-wheels:
name: Extension wheel (${{ matrix.extension }})
runs-on: ubuntu-latest
timeout-minutes: 30
# Compiles and runs PR-authored code, so it gets the read it needs and
# nothing else rather than inheriting the workflow's defaults.
permissions:
contents: read
# Installs the wheel that job built. An extension links no engine crate, so
# nothing here needs Vulkan, glslc or the engine's build set.
needs: wheel-interpreter-lifecycle
strategy:
fail-fast: false
matrix:
include:
- extension: streamlib-webrtc
native_module: streamlib_webrtc._native
- extension: streamlib-moq
native_module: streamlib_moq._native
steps:
- uses: actions/checkout@v4
with:
# Compiles and runs PR-authored code; a persisted checkout token
# would be readable by it.
persist-credentials: false
# `cargo test` builds the crate with pyo3's `extension-module` off — the
# default, so the crate's own unit tests can link libpython and run at
# all — which is what this is for.
- name: Install system dependencies
timeout-minutes: 6
uses: ./.github/actions/install-linux-engine-build-dependencies
with:
packages: python3-dev
# `streamlib-moq` builds `aws-lc-sys`, which needs cmake and a C
# compiler. The runner image ships both, and apt-installing cmake here
# instead costs a 13 MB download through the mirror this repo already had
# to put a time bound on — a bound that download exceeded. Assert it
# rather than fetch it, so a runner that ever drops cmake fails in seconds
# with a legible reason instead of inside an aws-lc-sys build script.
- name: The build tools the extension's native dependencies assume
run: |
cmake --version
cc --version
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: extension-${{ matrix.extension }}
workspaces: packages/${{ matrix.extension }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install uv
uses: astral-sh/setup-uv@v5
- uses: actions/download-artifact@v4
with:
name: streamlib-wheel
path: streamlib-wheel
# The engine as a binary dependency, exactly as a third party installs
# it: from a built wheel, never from this checkout's source.
- name: Install the engine wheel and build the extension into it
working-directory: packages/${{ matrix.extension }}
run: |
uv venv --python 3.12 .venv
VIRTUAL_ENV="$PWD/.venv" uv pip install pytest mypy typing_extensions
VIRTUAL_ENV="$PWD/.venv" uv pip install "$GITHUB_WORKSPACE"/streamlib-wheel/*.whl
VIRTUAL_ENV="$PWD/.venv" uvx maturin@1.9.6 develop
# The repo-root `cargo fmt --all` does not reach a non-member, so an
# extension checks its own.
- name: Formatting
working-directory: packages/${{ matrix.extension }}
run: cargo fmt --all --check
# Run before the venv is on PATH so pyo3 resolves the system interpreter,
# whose dev headers the apt step above installed.
#
# `--workspace`, not the root package alone: a crate vendored under the
# extension (`packages/streamlib-moq/vendor/moq-transport`) is an
# automatic member of the extension's workspace, and its own tests are
# where the patches it carries are pinned — a `cargo test --lib` from
# the root package selects that package alone and runs none of them.
- name: Extension crate tests
working-directory: packages/${{ matrix.extension }}
run: cargo test --locked --workspace --lib
- name: Clippy
working-directory: packages/${{ matrix.extension }}
run: cargo clippy --locked --lib --all-targets -- -D warnings
# The only check that compares the extension's hand-written `_native.pyi`
# to what it actually built. The engine workspace's gates do not walk a
# non-member, which is why an extension carries its own.
- name: Type stubs match the compiled module
working-directory: packages/${{ matrix.extension }}
run: .venv/bin/python -m mypy.stubtest ${{ matrix.native_module }}
- name: Type-check the extension's Python surface
working-directory: packages/${{ matrix.extension }}
run: uvx pyright@1.1.411
# Carries the portability gate over the built `.so`: an extension wheel
# installs beside the engine wheel on machines this project never sees,
# so it owes the same manylinux promise.
- name: Extension tests, GPU-free
working-directory: packages/${{ matrix.extension }}
run: |
.venv/bin/python -m pytest tests/ -v -m "not requires_gpu"
python-types:
name: Pyright (no build)
runs-on: ubuntu-latest
timeout-minutes: 10
# Deliberately independent of the wheel build. With `_engine.pyi` checked
# in, pyright reads the stub rather than the compiled module — it never
# imports anything — so this needs no Rust toolchain, no Vulkan, and no
# maturin, and finishes in seconds. It is also the check the author's editor
# runs: Pylance is pyright, so a green job here means working completion and
# correct hovers for anyone writing a processor.
steps:
- uses: actions/checkout@v4
with:
# Compiles and runs PR-authored code; a persisted checkout token
# would be readable by it. No workspace member has a git dependency.
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@v5
# pytest only, so its `py.typed` annotations resolve in `tests/`. The
# package under test is read from source; nothing is installed.
- name: Prepare the environment pyright resolves against
working-directory: sdk/streamlib-python-wheel
run: |
uv venv --python 3.12 .venv
VIRTUAL_ENV="$PWD/.venv" uv pip install pytest numpy pydantic typing_extensions
# CPU torch: the exchange tests type-check against torch's API, and
# pyright reports a missing import as an error. The cpu wheel keeps
# the download tractable for a no-build job.
VIRTUAL_ENV="$PWD/.venv" uv pip install torch --index-url https://download.pytorch.org/whl/cpu
- name: Type-check the wheel's Python surface
working-directory: sdk/streamlib-python-wheel
run: uvx pyright@1.1.411
# The release channel's index generator. Stdlib-only, so it rides the
# no-build job rather than waiting on a wheel it has nothing to do with —
# and a broken index is only otherwise visible at release time.
- name: Simple-index generator tests
working-directory: scripts
run: python3 -m unittest test_build_simple_index
# The audio loopback fixture's analysis half. numpy-only, so it rides
# this job for the same reason — it needs neither the wheel nor a built
# engine. The loopback itself needs a PipeWire session and stays
# rig-tier, but a fixture that cannot go red is worth nothing, so the
# negative controls that prove it can are gated here rather than on the
# rig where nobody would see them go stale.
- name: Audio loopback fixture negative controls
working-directory: runtime/streamlib-engine/tests/fixtures
run: >-
"$GITHUB_WORKSPACE/sdk/streamlib-python-wheel/.venv/bin/python"
-m unittest test_known_audio_signal