From b7fbca06e07719925830fbad2571cb6a5f8a65f7 Mon Sep 17 00:00:00 2001 From: Nick Price Date: Sat, 11 Jul 2026 17:42:18 -0700 Subject: [PATCH 1/6] hidapi: look for the unsuffixed libhidapi.so FreeBSD's comms/hidapi installs the library as libhidapi.so / libhidapi.so.0, without a backend suffix. None of the existing candidate names match it, so LoadLibrary exhausts the list and the module raises ImportError at import time, making Solaar unusable there. --- lib/hidapi/hidapi_impl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/hidapi/hidapi_impl.py b/lib/hidapi/hidapi_impl.py index d9fc3047..38ba3e4f 100644 --- a/lib/hidapi/hidapi_impl.py +++ b/lib/hidapi/hidapi_impl.py @@ -60,6 +60,8 @@ _library_paths = ( "libhidapi-hidraw.so.0", "libhidapi-libusb.so", "libhidapi-libusb.so.0", + "libhidapi.so", + "libhidapi.so.0", "libhidapi-iohidmanager.so", "libhidapi-iohidmanager.so.0", "libhidapi.dylib", From 057f7533b1ee642961509a5ffd1032ae0813f72c Mon Sep 17 00:00:00 2001 From: Nick Price Date: Sat, 11 Jul 2026 17:42:52 -0700 Subject: [PATCH 2/6] diversion: treat evdev as an optional dependency evdev was imported unconditionally on every OS except macOS and Windows, i.e. the code assumed "not macOS and not Windows" means Linux. On any other platform the import is a hard failure at startup. evdev is also genuinely optional: setup.py only installs it when platform_system == "Linux", and packagers may make it optional. Select on whether the module imports rather than on the OS name. click_uinput() dereferenced evdev.ecodes before calling simulate_uinput(), so it raised AttributeError rather than degrading; click() has no handler, so that escaped to the caller. Bail out early there and in setup_uinput() when evdev is absent. simulate_scroll() was already safe, but only because setup_uinput()'s blanket except swallowed the AttributeError. --- lib/logitech_receiver/diversion.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index e933f5ad..b55982e5 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -21,7 +21,6 @@ import logging import math import numbers import os -import platform import socket import struct import subprocess @@ -39,12 +38,13 @@ import yaml from keysyms import keysymdef -# There is no evdev on macOS or Windows. Diversion will not work without -# it but other Solaar functionality is available. -if platform.system() in ("Darwin", "Windows"): - evdev = None -else: +# evdev is not available on every platform, and is an optional dependency +# elsewhere. Diversion will not work without it but other Solaar +# functionality is available. +try: import evdev +except ImportError: + evdev = None from .common import NamedInt from .hidpp20 import SupportedFeature @@ -257,6 +257,8 @@ else: def setup_uinput(): global udevice + if evdev is None: + return False if udevice is not None: return udevice try: @@ -337,6 +339,8 @@ def simulate_key(code, event): # X11 keycode but Solaar event code def click_uinput(button, count): + if evdev is None: + return False if isinstance(count, int): for _ in range(count): if not simulate_uinput(evdev.ecodes.EV_KEY, button[1], 1): From 2cfddddd10d0f4afffa46c0c054f785f4d9f39ce Mon Sep 17 00:00:00 2001 From: Nick Price Date: Sat, 11 Jul 2026 17:43:02 -0700 Subject: [PATCH 3/6] diversion: do not name dbus in the handler that guards importing it gnome_dbus_interface_setup() imports dbus inside the try block but catches dbus.exceptions.DBusException. If the import itself raises ImportError -- dbus-python is only installed on Linux -- the name dbus is unbound, so the ImportError is not caught and merely evaluating the except clause raises NameError. Catch Exception instead. --- lib/logitech_receiver/diversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index b55982e5..026c7556 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -197,7 +197,7 @@ def gnome_dbus_interface_setup(): bus = dbus.SessionBus() remote_object = bus.get_object("org.gnome.Shell", "/io/github/pwr_solaar/solaar") _dbus_interface = dbus.Interface(remote_object, "io.github.pwr_solaar.solaar") - except dbus.exceptions.DBusException: + except Exception: logger.warning( "Solaar Gnome extension not installed - some rule capabilities inoperable", exc_info=sys.exc_info(), From 9da66068dc1f0b254f42062aae82e7014beeda9d Mon Sep 17 00:00:00 2001 From: Nick Price Date: Sat, 11 Jul 2026 17:43:14 -0700 Subject: [PATCH 4/6] dbus: do not use the bus in watch_suspend_resume when there is none The guard read if bus is not None and on_resume_callback is not None or on_suspend_callback is not None: which parses as (bus is not None and on_resume_callback is not None) or (on_suspend_callback is not None) so a caller passing on_suspend_callback entered the body regardless of whether the bus exists, and bus.add_signal_receiver() raised AttributeError on None. gtk.py passes both callbacks under --restart-on-wake-up, so that option crashed on any system without a running system dbus. Test the bus separately and return early. The success message was also logged even when no bus was connected. --- lib/solaar/dbus.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/solaar/dbus.py b/lib/solaar/dbus.py index 142b5904..b7145cba 100644 --- a/lib/solaar/dbus.py +++ b/lib/solaar/dbus.py @@ -61,7 +61,9 @@ def watch_suspend_resume( global _resume_callback, _suspend_callback _suspend_callback = on_suspend_callback _resume_callback = on_resume_callback - if bus is not None and on_resume_callback is not None or on_suspend_callback is not None: + if bus is None: + return + if on_resume_callback is not None or on_suspend_callback is not None: bus.add_signal_receiver( _suspend_or_resume, "PrepareForSleep", From 2568a295b684ddc40644a1697c3ceea7003a5440 Mon Sep 17 00:00:00 2001 From: Nick Price Date: Sat, 11 Jul 2026 17:43:14 -0700 Subject: [PATCH 5/6] listener: -p is a Linux-only getfacl flag FreeBSD's getfacl(1) has no -p, so the call fails and the permission diagnostic that EACCES is supposed to print is swallowed by the enclosing except. Pass -p only where it exists. --- lib/solaar/listener.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index f09a9f18..7d515dc9 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -19,6 +19,7 @@ from __future__ import annotations import errno import logging +import platform import subprocess import time import typing @@ -472,7 +473,9 @@ def _process_add(device_info: DeviceInfo, retry): except OSError as e: if e.errno == errno.EACCES: try: - output = subprocess.check_output(["getfacl", "-p", device_info.path], text=True) + # -p (don't strip leading '/') is a Linux getfacl extension + getfacl = ["getfacl", "-p"] if platform.system() == "Linux" else ["getfacl"] + output = subprocess.check_output([*getfacl, device_info.path], text=True) logger.warning("Missing permissions on %s\n%s.", device_info.path, output) except Exception: pass From 9bbe552c0a714a86b933ca62c61a0630f93509c6 Mon Sep 17 00:00:00 2001 From: Nick Price Date: Sat, 11 Jul 2026 17:43:21 -0700 Subject: [PATCH 6/6] solaar: require pyudev only where it is used, and support FreeBSD pyudev binds libudev and only the Linux HID backend imports it: base.py selects hidapi/udev_impl on Linux and hidapi/hidapi_impl everywhere else. But gtk.py demanded pyudev on every OS other than macOS and Windows and exited if it was missing, and setup.py listed it with no environment marker at all -- so on FreeBSD Solaar refused to start over a module the backend it had already chosen never touches. Gate the check on Linux, mark the dependency accordingly, and stop installing the udev rules on platforms that have no udev. FreeBSD uses devd and its own devfs rules for the same job. --- lib/solaar/gtk.py | 3 ++- setup.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py index e0680820..4bc3c624 100755 --- a/lib/solaar/gtk.py +++ b/lib/solaar/gtk.py @@ -157,7 +157,8 @@ def _handlesig(signl, stack): def main(): - if platform.system() not in ("Darwin", "Windows"): + # Only the Linux HID backend uses pyudev; other platforms use the hidapi backend. + if platform.system() == "Linux": _require("pyudev", "python3-pyudev") args = _parse_arguments() diff --git a/setup.py b/setup.py index 41506287..fbc2bf8b 100755 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +import platform import subprocess import textwrap @@ -35,7 +36,8 @@ def _data_files(): yield dirname(mo), [mo] yield "share/applications", ["share/applications/solaar.desktop"] - yield "lib/udev/rules.d", ["rules.d/42-logitech-unify-permissions.rules"] + if platform.system() == "Linux": # udev is Linux-only + yield "lib/udev/rules.d", ["rules.d/42-logitech-unify-permissions.rules"] yield "share/metainfo", ["share/solaar/io.github.pwr_solaar.solaar.metainfo.xml"] @@ -64,13 +66,14 @@ setup( "Natural Language :: English", "Programming Language :: Python :: 3 :: Only", "Operating System :: POSIX :: Linux", + "Operating System :: POSIX :: BSD :: FreeBSD", "Topic :: Utilities", ], - platforms=["linux"], + platforms=["linux", "freebsd"], python_requires=">=3.8", install_requires=[ 'evdev (>= 1.1.2) ; platform_system=="Linux"', - "pyudev (>= 0.13)", + 'pyudev (>= 0.13) ; platform_system=="Linux"', "PyYAML (>= 3.12)", "python-xlib (>= 0.27)", "psutil (>= 5.4.3)",