diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..ce94348 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,79 @@ +name: publish +concurrency: + group: ${{ github.workflow }} +on: # yamllint disable-line rule:truthy + release: + types: [published] + +jobs: + build-wheels: + if: ${{ github.repository == 'Dandelion-Science/python-evdev' }} + name: wheels on ${{ matrix.os }} + # evdev is Linux-only (needs ); no macOS/Windows wheels are + # possible. Native runners build their own arch (archs = "native"), no QEMU. + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, ubuntu-24.04-arm] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: pypa/cibuildwheel@v4.1.0 + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl + + build-sdist: + if: ${{ github.repository == 'Dandelion-Science/python-evdev' }} + runs-on: ubuntu-slim + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: astral-sh/setup-uv@v7 + - run: uv build --sdist + - uses: actions/upload-artifact@v4 + with: + name: cibw-sdist + path: dist/*.tar.gz + + publish: + needs: [build-wheels, build-sdist] + runs-on: ubuntu-slim + timeout-minutes: 15 + permissions: + id-token: write + contents: write + steps: + - uses: aws-actions/configure-aws-credentials@v6 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: ${{ vars.ARN_IAM_ROLE_GHA_CORE }} + - uses: actions/download-artifact@v4 + with: + path: dist + pattern: cibw-* + merge-multiple: true + - uses: softprops/action-gh-release@v3 + with: + files: dist/* + tag_name: ${{ github.event.release.tag_name }} + - name: Get CodeArtifact token + run: | + AWS_CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token \ + --domain ${{ vars.CODEARTIFACT_DOMAIN }} \ + --domain-owner ${{ vars.CODEARTIFACT_DOMAIN_OWNER }} \ + --query authorizationToken \ + --output text) + echo "AWS_CODEARTIFACT_TOKEN=$AWS_CODEARTIFACT_TOKEN" >> $GITHUB_ENV + - uses: Dandelion-Science/github-actions/setup-uv@main + - run: uv publish + env: + UV_PUBLISH_USERNAME: aws + UV_PUBLISH_PASSWORD: ${{ env.AWS_CODEARTIFACT_TOKEN }} + UV_PUBLISH_URL: ${{ vars.CODEARTIFACT_PYPI_STORE_REPOSITORY_URL }} diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml new file mode 100644 index 0000000..bea29b3 --- /dev/null +++ b/.github/workflows/pytest.yaml @@ -0,0 +1,34 @@ +name: pytest +concurrency: + group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} + cancel-in-progress: true +on: # yamllint disable-line rule:truthy + pull_request: + push: + branches: [main] + workflow_dispatch: + +jobs: + pytest: + if: ${{ !cancelled() && github.repository == 'Dandelion-Science/python-evdev' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }} + name: py${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.13", "3.13t", "3.14", "3.14t"] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: astral-sh/setup-uv@v7 + with: + activate-environment: true + python-version: ${{ matrix.python-version }} + - run: uv sync -q --no-default-groups --group test + # test_uinput.py needs root (opens /dev/uinput) and does not skip without it, + # so the rest of the suite runs as the normal user and uinput runs under sudo. + - run: pytest tests --ignore=tests/test_uinput.py + - run: sudo modprobe uinput + - run: sudo .venv/bin/pytest tests/test_uinput.py diff --git a/pyproject.toml b/pyproject.toml index d0b4f7c..8a404bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,12 +28,34 @@ classifiers = [ [project.urls] "Homepage" = "https://github.com/gvalkov/python-evdev" +[dependency-groups] +test = ["pytest"] + [tool.ruff] line-length = 120 [tool.ruff.lint] ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] +[tool.cibuildwheel] +# evdev is Linux-only (needs ): no macOS/Windows wheels exist. +# Build CPython 3.11-3.14 incl. the free-threaded 3.13t/3.14t builds; glibc only +# (musllinux skipped), CPython only (no PyPy). +build = ["cp311-*", "cp312-*", "cp313-*", "cp314-*", "cp313t-*", "cp314t-*"] +enable = ["cpython-freethreading"] +skip = ["*-musllinux*", "pp*"] +archs = "native" +build-frontend = "build[uv]" +# Smoke-test that the wheel imports and every compiled extension loads. +test-command = "python -c \"import evdev, evdev.ecodes, evdev._input, evdev._uinput, evdev._ecodes\"" + +[tool.cibuildwheel.linux] +# manylinux images are glibc/AlmaLinux based and normally ship kernel-headers +# (pulled in by glibc-devel), but evdev's build hard-fails without ; +# ensure they are present. No-op when already installed. +before-all = "yum install -y kernel-headers || true" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + [tool.bumpversion] current_version = "1.9.3" commit = true diff --git a/tests/test_free_threaded.py b/tests/test_free_threaded.py new file mode 100644 index 0000000..333c1ff --- /dev/null +++ b/tests/test_free_threaded.py @@ -0,0 +1,27 @@ +import os +import subprocess +import sys + +import pytest + + +def _is_freethreaded_build() -> bool: + import sysconfig + + return bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + + +@pytest.mark.skipif(not _is_freethreaded_build(), reason="requires free-threaded Python build") +def test_evdev_does_not_reenable_gil(): + # Run with PYTHON_GIL unset (not 0): PYTHON_GIL=0 force-disables the GIL + # regardless of the module, which would make this pass even for a C extension + # that has not declared Py_MOD_GIL_NOT_USED. We want the default behavior so + # importing a non-free-threaded extension actually re-enables the GIL and fails. + env = {k: v for k, v in os.environ.items() if k != "PYTHON_GIL"} + result = subprocess.run( + [sys.executable, "-c", "import sys; import evdev; assert not sys._is_gil_enabled()"], + capture_output=True, + text=True, + env=env, + ) + assert result.returncode == 0, f"evdev re-enabled the GIL:\n{result.stderr}"