Skip to content

fix(tools): raise TypeError for a non-boolean exclusive flag in the c… #181

fix(tools): raise TypeError for a non-boolean exclusive flag in the c…

fix(tools): raise TypeError for a non-boolean exclusive flag in the c… #181

Workflow file for this run

name: Develop CI
# Fast feedback for day-to-day work on develop: formatting, linting, and a
# single-platform (Linux/Debug) build + test. The heavy, thorough validation
# (multi-OS native, ASan/UBSan, coverage, WASM) runs as the pre-release gate in
# ci.yml on pull requests to main and pushes to main.
on:
push:
branches: [develop]
tags-ignore: ['**']
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- 'NOTICE'
pull_request:
branches: [develop]
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE'
- 'NOTICE'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
clang-format:
name: clang-format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install clang-format
run: |
sudo apt-get update
sudo apt-get install -y clang-format
- name: Check formatting
run: |
git ls-files -z -- '*.h' '*.hpp' '*.c' '*.cpp' '*.mm' ':!:third_party/**' |
xargs -0 clang-format --dry-run --Werror
lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: bindings/wasm
steps:
- uses: actions/checkout@v7
- run: corepack enable
working-directory: .
- uses: actions/setup-node@v7
with:
node-version: 22
cache: yarn
cache-dependency-path: |
bindings/wasm/yarn.lock
bindings/node/yarn.lock
- run: yarn install --immutable
- run: yarn lint
- name: Emit the WASM binding's declarations
# The Node type-check reads them: one cross-surface test drives the same
# input through Node, WASM and Python, so it imports the WASM entry
# point. Declarations only -- the emscripten module's types are tracked
# as src/sonare.js.d.ts, so tsc needs no toolchain and dist/sonare.wasm
# is left alone.
run: yarn build:types
- name: Lint and type-check the Node binding
# Neither check needs the native addon built: tsconfig.test.json
# excludes dist/ and build/, and the tests import from ../src rather
# than the package entry point.
working-directory: bindings/node
run: |
yarn install --immutable
yarn lint
yarn test:types
- name: Lint Python
# Pinned to the resolved version in requirements-dev.lock: an unpinned
# install lets a new ruff release fail an unrelated pull request.
# Invoked as a module so the check does not depend on pip's script
# directory being on PATH.
#
# The repo rather than a path list, matching `make lint`. Python lives
# in ten trees here and only the binding was gated, so a lint error
# anywhere else -- `tools/`, `benchmarks/`, `tests/` -- reached develop
# unseen. Ruff respects .gitignore and skips node_modules by default.
working-directory: .
run: |
python3 -m pip install --disable-pip-version-check ruff==0.16.8
python3 -m ruff check .
- name: Type-check the Python binding
# main-ci type-checks the stubs, but only on main, so type errors used
# to accumulate on develop and surface all at once at release time.
# Source-only like the ruff step: mypy_path resolves libsonare from
# src/, so the package itself is neither installed nor built. numpy is
# needed even though the config skips following it -- the override only
# applies once the module resolves, and without it every importing
# module reports import-not-found. Both are pinned for the reason ruff
# is, and mypy in particular reports new errors on unchanged code with
# every release.
#
# The second invocation is the non-vacuity guard: a file that must not
# type-check, so a misconfiguration that silently checks nothing fails
# here rather than passing everything.
working-directory: bindings/python
run: |
python3 -m pip install --disable-pip-version-check mypy==2.3.1 numpy==2.4.6
python3 -m mypy --strict src/libsonare ../../tests/typing/python_smoke.py
if python3 -m mypy --strict ../../tests/typing/python_catalog_invalid.py; then
echo "python_catalog_invalid.py unexpectedly passed mypy" >&2
exit 1
fi
- name: Drift gates
# The mechanical cross-surface gates, all read-only and stdlib-only
# except numpy: the bank-policy gate imports the shipping
# `tools/voicematch/status.py`, which reaches numpy through `capture.py`.
# Invoked through the Makefile so this list cannot drift from it: a
# hand-copied one silently omits whatever gate is added to `conformance`
# next, and reports green while doing it.
#
# `gen_abi_layout.py --check` stays a direct call, because the Makefile
# wraps it in rye -- which the runner does not have and the script does
# not need. It is the only gate needing a compiler, and it builds a
# small probe from the public C headers rather than the project, so this
# job stays build-free. It is also the load-bearing one: it compares
# every ctypes Structure against sizeof/alignof/offsetof taken from the
# C headers, and a desync there segfaults the Python binding rather than
# failing a test.
#
# `parity` depends on `conformance`, whose closing live-CLI comparison
# skips itself when no built CLI is present, as it is here.
working-directory: .
run: |
python3 -m pip install --disable-pip-version-check numpy==2.4.6
python3 tools/abi/gen_abi_layout.py --check
make check-abi-version
make processor-types-check
make parity
voicematch:
name: Calibration harness
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Run the calibration harness self-tests
# `tools/` sits outside ctest and outside the drift gates, and this
# workflow runs ctest directly rather than `make test`, so nothing else
# here exercises the harness the instrument bank is calibrated with.
#
# The cases read only the tracked capture definitions and reference
# profiles -- no rendered corpus, no plugin, no built library -- so this
# job stays build-free. Two skip themselves when there is no shared
# library to render with.
#
# Invoked directly rather than through `make test-voicematch`, which
# wraps the same command in rye for a dylib these cases do not need.
# Both versions are the ones resolved in requirements-dev.lock, pinned
# for the reason ruff is: an unpinned install lets a new release fail an
# unrelated pull request.
run: |
python3 -m pip install --disable-pip-version-check pytest==9.1.1 numpy==2.4.6
python3 -m pytest tools/voicematch tools/audition -q
build-and-test:
name: Native C++ (ubuntu-latest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake build-essential libeigen3-dev
- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DBUILD_CLI=OFF -DSONARE_WITH_FFMPEG=OFF
- name: Build
# Capped at 2 jobs: unbounded parallelism OOM-kills cc1plus on the
# 4-core/16GB hosted runners when compiling the large Debug test TUs.
run: cmake --build build --parallel 2
- name: Generate K-weighting reference fixture
run: make fixtures
- name: Test
run: ctest --test-dir build --output-on-failure --parallel
# `make test-golden` is deliberately not run here. The golden hashes
# quantize samples at 1e-6 before folding them into FNV-1a, which is
# finer than the float reproducibility across architectures and libm
# implementations, so a hash generated on one host cannot match another.
# They stay a same-environment regression check, run locally.
- name: Check capability catalog
run: make capability-catalog-check
examples:
name: Examples (Python and C++)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.13'
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake build-essential libeigen3-dev
python -m pip install numpy
- name: Generate a short WAV fixture
run: |
python - <<'PY'
import math
import struct
import wave
with wave.open('example-input.wav', 'wb') as output:
output.setnchannels(1)
output.setsampwidth(2)
output.setframerate(48000)
output.writeframes(b''.join(
struct.pack('<h', round(0.4 * 32767 * math.sin(2 * math.pi * 440 * i / 48000)))
for i in range(48000 * 3)
))
PY
- name: Build the Python shared library
run: |
cmake -B build-examples -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED=ON \
-DBUILD_TESTING=OFF -DBUILD_CLI=OFF -DSONARE_WITH_FFMPEG=OFF
cmake --build build-examples --target sonare_shared --parallel 2
cp build-examples/lib/libsonare.so bindings/python/src/libsonare/
- name: Run Python examples
env:
PYTHONPATH: bindings/python/src
run: |
python examples/python/analyze.py example-input.wav
python examples/python/master.py example-input.wav example-mastered.wav
python examples/python/midi_to_wav.py example-midi.wav
test -s example-mastered.wav
test -s example-midi.wav
- name: Build and run the C++ example
run: |
cmake -S examples/cpp -B build-examples-cpp -DBUILD_CLI=OFF -DBUILD_TESTING=OFF \
-DSONARE_WITH_FFMPEG=OFF
cmake --build build-examples-cpp --parallel 2
build-examples-cpp/analyze example-input.wav
install-package:
# The C++ distribution path, checked the way a consumer meets it: install
# into a scratch prefix, then configure a project that knows nothing but
# find_package(sonare). In-tree builds link every target together, so an
# incomplete link interface or a missing installed header cannot show up
# anywhere else. The macOS half of this runs on the main-branch workflow.
name: Installed package (find_package)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Linux dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake build-essential libeigen3-dev pkg-config
- name: Build and install into a scratch prefix
run: |
cmake -B build-install -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF \
-DBUILD_SHARED=ON -DSONARE_WITH_FFMPEG=OFF \
-DCMAKE_INSTALL_PREFIX="$PWD/install-prefix"
cmake --build build-install --parallel 2
cmake --install build-install
- name: Consume it from a project that only knows find_package
run: |
cmake -S tests/cmake/consumer -B build-install-consumer \
-DCMAKE_PREFIX_PATH="$PWD/install-prefix"
cmake --build build-install-consumer --parallel 2
ctest --test-dir build-install-consumer --output-on-failure --no-tests=error
- name: Check the pkg-config file
run: |
PC_DIR="$(dirname "$(find "$PWD/install-prefix" -name sonare.pc | head -1)")"
PKG_CONFIG_PATH="$PC_DIR" pkg-config --validate sonare
PKG_CONFIG_PATH="$PC_DIR" pkg-config --modversion sonare
PKG_CONFIG_PATH="$PC_DIR" pkg-config --cflags --libs sonare
python-wheel:
name: Python wheel (manylinux x86_64)
runs-on: ubuntu-latest
container: quay.io/pypa/manylinux_2_28_x86_64
steps:
- uses: actions/checkout@v7
- name: Build, audit, and test wheel
# The manylinux image has no system Eigen package; the top-level CMake
# FetchContent fallback supplies its header-only dependency.
run: |
cmake -B build-wheel -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED=ON \
-DBUILD_TESTING=OFF -DBUILD_CLI=OFF \
-DSONARE_WITH_FFMPEG=OFF
cmake --build build-wheel --target sonare_shared --config Release --parallel
cp build-wheel/lib/libsonare.so bindings/python/src/libsonare/
cd bindings/python
/opt/python/cp313-cp313/bin/python -m pip install wheel hatchling auditwheel pytest
/opt/python/cp313-cp313/bin/python -m pip wheel . --no-deps -w dist-raw/
auditwheel repair dist-raw/*.whl -w dist/ --plat manylinux_2_28_x86_64
auditwheel show dist/*.whl
/opt/python/cp313-cp313/bin/python -m pip install dist/*.whl
/opt/python/cp313-cp313/bin/python -c "import libsonare; libsonare.version()"
# The wheel is a shipped artifact, so run its suite against the
# repaired wheel here rather than for the first time at publish time.
# pytest is installed above; `-m "not slow"` comes from pyproject.
/opt/python/cp313-cp313/bin/python -m pytest tests/ -x -q