From e26ea758f30a21c74427ab13bd5436e2c4865a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Mon, 27 Jul 2026 20:34:41 +0200 Subject: [PATCH] feat: add Linux binary wheels Ship self-contained manylinux wheels without pulling optional Gammu dependencies into every installation. This lets Linux users install without a system Gammu package while preserving source builds for full functionality. --- .github/renovate.json | 28 ++++++++++++- .github/scripts/build-gammu-wheel.sh | 59 ++++++++++++++++++++++++++++ .github/workflows/setup.yml | 42 ++++++++++++++++++-- README.rst | 28 ++++++++++++- gammu/src/gammu.c | 18 ++++++++- pyproject.toml | 13 ++++++ setup.py | 18 +++++---- test/test_smsd.py | 33 ++++++++++++++++ 8 files changed, 225 insertions(+), 14 deletions(-) create mode 100755 .github/scripts/build-gammu-wheel.sh diff --git a/.github/renovate.json b/.github/renovate.json index cf0c48ee7..b8ff8710c 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,4 +1,30 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>WeblateOrg/meta:renovate"] + "extends": ["github>WeblateOrg/meta:renovate"], + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.github/scripts/build-gammu-wheel\\.sh$/" + ], + "matchStrings": [ + "# renovate: datasource=github-release-attachments depName=gammu/gammu versioning=loose\\nreadonly GAMMU_VERSION=\"(?[^\"]+)\"\\nreadonly GAMMU_SHA256=\"(?[a-f0-9]{64})\"" + ], + "datasourceTemplate": "github-release-attachments", + "depNameTemplate": "gammu/gammu", + "versioningTemplate": "loose", + "autoReplaceStringTemplate": "# renovate: datasource=github-release-attachments depName=gammu/gammu versioning=loose\nreadonly GAMMU_VERSION=\"{{{newValue}}}\"\nreadonly GAMMU_SHA256=\"{{{newDigest}}}\"" + }, + { + "customType": "regex", + "managerFilePatterns": ["/^README\\.rst$/"], + "matchStrings": [ + "\\.\\. renovate: datasource=github-release-attachments depName=gammu/gammu versioning=loose\\n\\nThe bundled Gammu (?[^\\s]+) library is licensed under GPL-2\\.0-or-later\\. The\\ncorresponding source archive is available from:\\n\\nhttps://github\\.com/gammu/gammu/releases/download/[^/\\s]+/Gammu-[^/\\s]+\\.tar\\.gz" + ], + "datasourceTemplate": "github-release-attachments", + "depNameTemplate": "gammu/gammu", + "versioningTemplate": "loose", + "autoReplaceStringTemplate": ".. renovate: datasource=github-release-attachments depName=gammu/gammu versioning=loose\n\nThe bundled Gammu {{{newValue}}} library is licensed under GPL-2.0-or-later. The\ncorresponding source archive is available from:\n\nhttps://github.com/gammu/gammu/releases/download/{{{newValue}}}/Gammu-{{{newValue}}}.tar.gz" + } + ] } diff --git a/.github/scripts/build-gammu-wheel.sh b/.github/scripts/build-gammu-wheel.sh new file mode 100755 index 000000000..589874c52 --- /dev/null +++ b/.github/scripts/build-gammu-wheel.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# Build the deliberately minimal Gammu libraries used by Linux wheels. + +set -euo pipefail + +# renovate: datasource=github-release-attachments depName=gammu/gammu versioning=loose +readonly GAMMU_VERSION="1.43.3" +readonly GAMMU_SHA256="a340b8347f5b30c84aa2a48fc497560fdc0d613618baa14b1bad94b3f316c7ff" +readonly GAMMU_PREFIX="${GAMMU_WHEEL_PREFIX:-/opt/python-gammu-gammu}" +readonly GAMMU_ARCHIVE="Gammu-${GAMMU_VERSION}.tar.gz" +readonly GAMMU_URL="https://github.com/gammu/gammu/releases/download/${GAMMU_VERSION}/${GAMMU_ARCHIVE}" + +if ! command -v cmake >/dev/null 2>&1; then + dnf install -y cmake +fi + +build_root="$(mktemp -d /tmp/python-gammu-wheel.XXXXXX)" +trap 'rm -rf "$build_root"' EXIT + +curl --fail --location --silent --show-error \ + --output "${build_root}/${GAMMU_ARCHIVE}" \ + "$GAMMU_URL" +printf "%s %s\n" "$GAMMU_SHA256" "${build_root}/${GAMMU_ARCHIVE}" \ + | sha256sum --check - +tar --extract --file "${build_root}/${GAMMU_ARCHIVE}" --directory "$build_root" + +cmake \ + -S "${build_root}/Gammu-${GAMMU_VERSION}" \ + -B "${build_root}/build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DWITH_BLUETOOTH=OFF \ + -DWITH_USB=OFF \ + -DWITH_IRDA=OFF \ + -DWITH_MySQL=OFF \ + -DWITH_ODBC=OFF \ + -DWITH_Postgres=OFF \ + -DWITH_LibDBI=OFF \ + -DWITH_CURL=OFF \ + -DWITH_Glib=OFF \ + -DWITH_GObject=OFF \ + -DWITH_SystemD=OFF \ + -DWITH_Libintl=OFF \ + -DWITH_Iconv=OFF \ + -DWITH_Gettext=OFF \ + -DWITH_Doxygen=OFF \ + -DWITH_BashCompletion=OFF +cmake --build "${build_root}/build" \ + --parallel \ + --target libGammu gsmsd + +install -d "${GAMMU_PREFIX}/include/gammu" "${GAMMU_PREFIX}/lib" +install -m 0644 "${build_root}/build/include/"*.h \ + "${GAMMU_PREFIX}/include/gammu/" +install -m 0644 "${build_root}/build/libgammu/libGammu.a" \ + "${build_root}/build/smsd/libgsmsd.a" \ + "${GAMMU_PREFIX}/lib/" diff --git a/.github/workflows/setup.yml b/.github/workflows/setup.yml index f3523a3ae..780d9eab2 100644 --- a/.github/workflows/setup.yml +++ b/.github/workflows/setup.yml @@ -6,7 +6,7 @@ permissions: contents: read jobs: - linux: + sdist: runs-on: ubuntu-24.04 steps: @@ -19,12 +19,34 @@ jobs: - uses: ./.github/actions/install-gammu - name: build run: uv build --sdist - - name: Store the binary wheel + - name: Store the source distribution uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: dist-linux + name: dist-sdist path: dist + linux: + strategy: + matrix: + include: + - architecture: x86_64 + runner: ubuntu-24.04 + - architecture: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Build wheels + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + env: + CIBW_ARCHS_LINUX: ${{ matrix.architecture }} + - name: Store the Linux wheels + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: dist-linux-${{ matrix.architecture }} + path: wheelhouse + windows: runs-on: windows-latest strategy: @@ -55,6 +77,7 @@ jobs: check: needs: - linux + - sdist - windows runs-on: ubuntu-24.04 steps: @@ -75,6 +98,17 @@ jobs: merge-multiple: true - run: uvx twine check --strict dist/* - run: uvx abi3audit dist/*abi3*.whl + - name: Check Linux wheel contents + shell: bash + run: | + test "$(find dist -maxdepth 1 -name '*manylinux*.whl' | wc -l)" -eq 10 + for wheel in dist/*manylinux*.whl; do + uvx auditwheel show "$wheel" + if unzip -Z1 "$wheel" | grep -E '(^|/)[^/]+[.]libs/'; then + echo "Unexpected bundled shared libraries in $wheel" + exit 1 + fi + done - run: uvx pydistcheck --inspect dist/* - run: uvx check-wheel-contents dist/*.whl - run: uvx check-manifest -v @@ -87,6 +121,7 @@ jobs: needs: - check - linux + - sdist - windows if: startsWith(github.ref, 'refs/tags/') && github.repository == 'gammu/python-gammu' @@ -107,6 +142,7 @@ jobs: needs: - check - linux + - sdist - windows permissions: contents: write diff --git a/README.rst b/README.rst index 3fb1640a0..40fd9dfbd 100644 --- a/README.rst +++ b/README.rst @@ -45,11 +45,35 @@ Install it using pip installer:: pip install python-gammu +Linux wheels +============ + +Binary wheels are available for CPython 3.10 and newer on 64-bit x86 and ARM +Linux systems using glibc 2.28 or newer. They include a minimal, statically +linked Gammu library, so installing Gammu separately is not necessary. + +The wheels support core phone protocols, serial connections, converters, +backups, and the SMSD file and null services. Bluetooth, native USB, IrDA, +database-backed SMSD services, CURL, GLib/GObject, systemd, gettext, and iconv +integration are not included. + +.. renovate: datasource=github-release-attachments depName=gammu/gammu versioning=loose + +The bundled Gammu 1.43.3 library is licensed under GPL-2.0-or-later. The +corresponding source archive is available from: + +https://github.com/gammu/gammu/releases/download/1.43.3/Gammu-1.43.3.tar.gz + +To use those optional features, install a fully featured Gammu development +package and force a source build:: + + pip install --no-binary python-gammu python-gammu + Requirements ============ -To compile python-gammu, you need Gammu development files (usually shipped as -``libgammu-dev`` or ``gammu-devel`` in Linux distributions). +To compile python-gammu from source, you need Gammu development files (usually +shipped as ``libgammu-dev`` or ``gammu-devel`` in Linux distributions). The location of the libraries is discovered using ``pkg-config``, ``GAMMU_PATH`` environment variable and falls back to generic locations. In diff --git a/gammu/src/gammu.c b/gammu/src/gammu.c index 8361ff73b..38b0d5fc0 100644 --- a/gammu/src/gammu.c +++ b/gammu/src/gammu.c @@ -5455,7 +5455,23 @@ StateMachine_str(StateMachineObject *self) static void StateMachine_dealloc(StateMachineObject *self) { - BEGIN_PHONE_COMM + if (self->in_callback) { + PyObject *error_type = NULL; + PyObject *error_value = NULL; + PyObject *error_traceback = NULL; + + PyErr_Fetch(&error_type, &error_value, &error_traceback); + PyErr_SetString(PyExc_RuntimeError, "Can not call Gammu functions from within callback"); + PyErr_WriteUnraisable((PyObject *)self); + PyErr_Clear(); + PyErr_Restore(error_type, error_value, error_traceback); + return; + } + +#ifdef WITH_THREAD + Py_BEGIN_ALLOW_THREADS + PyThread_acquire_lock(self->mutex, 1); +#endif if (GSM_IsConnected(self->s)) { /* Disable any possible incoming notifications */ GSM_SetIncomingSMS(self->s, FALSE); diff --git a/pyproject.toml b/pyproject.toml index 32353d00d..8b48f2fe2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,19 @@ ignore = [ "admin/*" ] +[tool.cibuildwheel] +build = "cp3{10,11,12,13,14}-manylinux*" +test-command = "pytest test" +test-requires = ["pytest==9.1.1"] +test-sources = ["test"] + +[tool.cibuildwheel.linux] +before-all = "bash {package}/.github/scripts/build-gammu-wheel.sh" +environment = {GAMMU_PATH = "/opt/python-gammu-gammu"} +manylinux-aarch64-image = "manylinux_2_28" +manylinux-x86_64-image = "manylinux_2_28" +repair-wheel-command = "auditwheel repair --strip -w {dest_dir} {wheel}" + [tool.coverage.paths] source = [ "." diff --git a/setup.py b/setup.py index d4127740e..3168d7dc8 100755 --- a/setup.py +++ b/setup.py @@ -117,7 +117,7 @@ def get_libs(self) -> list[str]: ["pkg-config", "--libs-only-l", "gammu", "gammu-smsd"] ).decode("utf-8") return output.replace("-l", "").strip().split() - libs = ["Gammu", "gsmsd"] + libs = ["gsmsd", "Gammu"] if self.on_windows: libs.extend(("Advapi32", "shfolder", "shell32")) else: @@ -186,12 +186,16 @@ def get_module(): "gammu/src/smsd.c", ], ) - flags = config.get_cflags() - if flags: - module.extra_compile_args.append(flags) - flags = config.get_ldflags() - if flags: - module.extra_link_args.append(flags) + if config.use_pkgconfig: + flags = config.get_cflags() + if flags: + module.extra_compile_args.append(flags) + flags = config.get_ldflags() + if flags: + module.extra_link_args.append(flags) + else: + module.include_dirs.append((config.path / "include" / "gammu").as_posix()) + module.library_dirs.append((config.path / "lib").as_posix()) return module diff --git a/test/test_smsd.py b/test/test_smsd.py index 9e4d2aff3..09ea1fc3d 100644 --- a/test/test_smsd.py +++ b/test/test_smsd.py @@ -45,6 +45,39 @@ MAX_STATUS_RETRIES = 2 +@pytest.mark.parametrize("service", ["files", "null"]) +def test_builtin_service(tmp_path: Path, service: str) -> None: + """Verify SMSD services included in the minimal Linux wheels.""" + paths = {} + for name in ("inbox", "outbox", "sent", "error"): + path = tmp_path / name + path.mkdir() + paths[name] = f"{path.as_posix()}/" + + config = tmp_path / "smsdrc" + dummy_path = (Path(__file__).parent / "data" / "gammu-dummy").as_posix() + config.write_text( + f""" +[gammu] +model = dummy +connection = none +port = {dummy_path} +gammuloc = /dev/null + +[smsd] +service = {service} +inboxpath = {paths["inbox"]} +outboxpath = {paths["outbox"]} +sentsmspath = {paths["sent"]} +errorsmspath = {paths["error"]} +""", + encoding="utf-8", + ) + + smsd = gammu.smsd.SMSD(config.as_posix()) + del smsd + + def get_script() -> Path: """ Returns SQL script to create database.