diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9148108..8883155 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ on: workflow_dispatch: env: - LSL_RELEASE_URL: "https://github.com/sccn/liblsl/releases/download/v1.16.2" - LSL_RELEASE: "1.16.2" + LSL_RELEASE_URL: "https://github.com/sccn/liblsl/releases/download/v1.17.4" + LSL_RELEASE: "1.17.4" jobs: @@ -19,10 +19,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/ruff-action@v1 - - uses: astral-sh/ruff-action@v1 - with: - args: "format --check" + - uses: astral-sh/setup-uv@v4 + - run: uv run ruff check + - run: uv run ruff format --check test: needs: style @@ -37,16 +36,18 @@ jobs: - uses: actions/checkout@v4 - name: Download liblsl (Windows) if: matrix.os == 'windows-latest' + shell: bash run: | - curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-Win_amd64.zip -o liblsl.zip - unzip -oj liblsl.zip bin/lsl* -d src/pylsl/lib/ + curl -L ${LSL_RELEASE_URL}/liblsl-${LSL_RELEASE}-Win_amd64.zip -o liblsl.zip + unzip -oj liblsl.zip '*/bin/lsl.dll' -d src/pylsl/lib/ - name: Install liblsl (MacOS) if: matrix.os == 'macos-latest' - run: brew install labstreaminglayer/tap/lsl + run: | + brew install labstreaminglayer/tap/lsl + echo "PYLSL_LIB=$(brew --prefix lsl)/Frameworks/lsl.framework/Versions/A/lsl" >> $GITHUB_ENV - name: Install liblsl (Ubuntu) if: startsWith(matrix.os, 'ubuntu-') run: | - sudo apt install -y libpugixml-dev curl -L ${LSL_RELEASE_URL}/liblsl-${LSL_RELEASE}-$(lsb_release -sc)_amd64.deb -o liblsl.deb sudo apt install ./liblsl.deb - name: Install uv diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index b759a7d..1ee0acc 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -3,56 +3,151 @@ name: Build and publish Python 🐍 distributions 📦 to PyPI on: release: types: [published] + pull_request: + branches: + - main workflow_dispatch: env: - LSL_RELEASE_URL: "https://github.com/sccn/liblsl/releases/download/" - LSL_RELEASE: "1.16.2" + LSL_RELEASE_URL: "https://github.com/sccn/liblsl/releases/download/v1.17.4" + LSL_RELEASE: "1.17.4" defaults: run: shell: bash jobs: - deploy: - name: ${{ matrix.config.name }} + # Build pure Python wheel (no bundled library) as fallback + build-pure: + name: Build pure Python wheel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Needed for setuptools-scm + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Build pure Python wheel + run: uv build + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-pure + path: dist/* + + # Build platform-specific wheels with bundled liblsl + build-platform: + name: Build ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} - permissions: - id-token: write strategy: fail-fast: false matrix: config: - - name: "ubuntu-24.04" - os: "ubuntu-latest" - pyarch: "x64" - - name: "windows-x64" - os: "windows-latest" - arch: "amd64" - pyarch: "x64" - - name: "windows-x86" - os: "windows-latest" - arch: "i386" - pyarch: "x86" + - name: "Windows x64" + os: windows-latest + arch: amd64 + pyarch: x64 + asset: "liblsl-${LSL_RELEASE}-Win_amd64.zip" + extract: "unzip -oj liblsl.zip '*/bin/lsl.dll' -d src/pylsl/lib" + - name: "Windows x86" + os: windows-latest + arch: i386 + pyarch: x86 + asset: "liblsl-${LSL_RELEASE}-Win_i386.zip" + extract: "unzip -oj liblsl.zip '*/bin/lsl.dll' -d src/pylsl/lib" + - name: "macOS universal" + os: macos-latest + arch: universal + pyarch: x64 + asset: "lsl.xcframework.1.17.zip" + extract: | + unzip xcframework.zip + cp lsl.xcframework/macos-arm64_x86_64/lsl.framework/Versions/A/lsl src/pylsl/lib/liblsl.dylib + codesign -s - -f src/pylsl/lib/liblsl.dylib + - name: "Linux x86_64" + os: ubuntu-22.04 + arch: x86_64 + pyarch: x64 + asset: "liblsl-${LSL_RELEASE}-jammy_amd64.deb" + extract: | + sudo apt-get update && sudo apt-get install -y libpugixml1v5 + ar x liblsl.deb + tar -xf data.tar.* --wildcards '*/liblsl.so*' + cp ./usr/lib/liblsl.so* src/pylsl/lib/ + repair: true steps: - uses: actions/checkout@v4 - - name: Download liblsl (Windows) - if: matrix.config.os == 'windows-latest' + with: + fetch-depth: 0 # Needed for setuptools-scm + + - name: Download liblsl + run: | + ASSET="${{ matrix.config.asset }}" + ASSET="${ASSET//\$\{LSL_RELEASE\}/$LSL_RELEASE}" + if [[ "$ASSET" == *xcframework* ]]; then + curl -L "${LSL_RELEASE_URL}/${ASSET}" -o xcframework.zip + elif [[ "$ASSET" == *.zip ]]; then + curl -L "${LSL_RELEASE_URL}/${ASSET}" -o liblsl.zip + elif [[ "$ASSET" == *.deb ]]; then + curl -L "${LSL_RELEASE_URL}/${ASSET}" -o liblsl.deb + else + curl -L "${LSL_RELEASE_URL}/${ASSET}" -o liblsl.tar.gz + fi + + - name: Extract liblsl run: | - curl -L ${LSL_RELEASE_URL}/v${LSL_RELEASE}/liblsl-${LSL_RELEASE}-Win_${{ matrix.config.arch}}.zip -o liblsl.zip - unzip -oj liblsl.zip bin/lsl* -d src/pylsl/lib - - name: Set up Python 3.x - uses: actions/setup-python@v4 + mkdir -p src/pylsl/lib + ${{ matrix.config.extract }} + + - name: List bundled library + run: ls -la src/pylsl/lib/ + + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: "3.x" architecture: ${{ matrix.config.pyarch }} + - name: Install uv uses: astral-sh/setup-uv@v4 - - name: Build Package (Linux) - if: matrix.config.os != 'windows-latest' - run: uv build - - name: Build Package (Windows) - if: matrix.config.os == 'windows-latest' + + - name: Build wheel run: uv build --wheel - - name: Publish package distributions to PyPI - run: uv publish + + - name: Repair wheel (Linux) + if: matrix.config.repair + run: | + pip install auditwheel patchelf + auditwheel repair dist/*.whl -w dist/ + rm dist/*-none-any.whl + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.config.os }}-${{ matrix.config.arch }} + path: dist/*.whl + + # Publish all wheels to PyPI + publish: + name: Publish to PyPI + needs: [build-pure, build-platform] + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: Download all wheels + uses: actions/download-artifact@v4 + with: + path: dist + pattern: wheels-* + merge-multiple: true + + - name: List wheels + run: ls -la dist/ + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Publish to PyPI + if: github.event_name == 'release' + run: uv publish dist/* diff --git a/pyproject.toml b/pyproject.toml index 9c568ea..4b584b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ examples = [ [dependency-groups] dev = [ "pytest>=8.3.4", - "ruff>=0.8.2", + "ruff>=0.14", ] [build-system] @@ -65,4 +65,4 @@ license-files = [] version_file = "src/pylsl/__version__.py" [tool.setuptools.package-data] -pylsl = ["lib/*.dll"] +pylsl = ["lib/*.dll", "lib/*.so*", "lib/*.dylib"] diff --git a/src/pylsl/info.py b/src/pylsl/info.py index bc2ebf3..ad089fb 100644 --- a/src/pylsl/info.py +++ b/src/pylsl/info.py @@ -92,7 +92,7 @@ def __init__( ) self.obj = ctypes.c_void_p(self.obj) if not self.obj: - raise RuntimeError("could not create stream description " "object.") + raise RuntimeError("could not create stream description object.") def __del__(self): """Destroy a previously created StreamInfo object.""" diff --git a/src/pylsl/inlet.py b/src/pylsl/inlet.py index 472733b..b9ebc77 100644 --- a/src/pylsl/inlet.py +++ b/src/pylsl/inlet.py @@ -58,9 +58,7 @@ def __init__( flags. Use `proc_ALL` for all flags. (default proc_none). """ if type(info) is list: - raise TypeError( - "description needs to be of type StreamInfo, " "got a list." - ) + raise TypeError("description needs to be of type StreamInfo, got a list.") self.obj = lib.lsl_create_inlet(info.obj, max_buflen, max_chunklen, recover) self.obj = ctypes.c_void_p(self.obj) if not self.obj: diff --git a/src/pylsl/lib/__init__.py b/src/pylsl/lib/__init__.py index ad7314a..a81de98 100644 --- a/src/pylsl/lib/__init__.py +++ b/src/pylsl/lib/__init__.py @@ -257,14 +257,14 @@ def find_liblsl_libraries(verbose=False): lib.lsl_pull_chunk_str.restype = ctypes.c_long lib.lsl_pull_chunk_buf.restype = ctypes.c_long except Exception: - print("pylsl: chunk transfer functions not available in your liblsl " "version.") + print("pylsl: chunk transfer functions not available in your liblsl version.") # noinspection PyBroadException try: lib.lsl_create_continuous_resolver.restype = ctypes.c_void_p lib.lsl_create_continuous_resolver_bypred.restype = ctypes.c_void_p lib.lsl_create_continuous_resolver_byprop.restype = ctypes.c_void_p except Exception: - print("pylsl: ContinuousResolver not (fully) available in your liblsl " "version.") + print("pylsl: ContinuousResolver not (fully) available in your liblsl version.") # int64 support on windows and 32bit OSes isn't there yet diff --git a/src/pylsl/util.py b/src/pylsl/util.py index c6f233c..1a68433 100644 --- a/src/pylsl/util.py +++ b/src/pylsl/util.py @@ -122,6 +122,7 @@ def deprecated(reason: str = None): def old_function(): pass """ + def decorator(func): message = f"Function '{func.__name__}' is deprecated." if reason: @@ -130,11 +131,7 @@ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): warnings.simplefilter("always", DeprecationWarning) # ensure it shows up - warnings.warn( - message, - category=DeprecationWarning, - stacklevel=2 - ) + warnings.warn(message, category=DeprecationWarning, stacklevel=2) warnings.simplefilter("default", DeprecationWarning) return func(*args, **kwargs)