Skip to content

recipes: zeroconf 0.150.0 + ifaddr 0.2.0 - #112

Merged
ndonkoHenri merged 5 commits into
mainfrom
zeroconf
Aug 1, 2026
Merged

recipes: zeroconf 0.150.0 + ifaddr 0.2.0#112
ndonkoHenri merged 5 commits into
mainfrom
zeroconf

Conversation

@ndonkoHenri

Copy link
Copy Markdown

Adds zeroconf 0.150.0 (mDNS/DNS-SD, requested in flet-dev/flet#6738 and ifaddr 0.2.0, its only runtime dependency, which needs a one-line iOS fix despite being pure Python.

zeroconf — Cython-accelerated pure-Python package: 18 accelerator modules compiled from the runtime .py files themselves (pure-Python-mode Cython, self-contained C, no external libs). First poetry-core-backend recipe; PythonPackageBuilder handles it unchanged. Upstream's build_ext.py swallows extension-compile errors even with REQUIRE_CYTHON set, so the recipe sets REQUIRE_CYTHON=1 plus a 2-line fail-loud patch, and the tests assert the compiled modules are real extensions (including zeroconf/_services/__init__ — a native subpackage __init__, which serious-python loads correctly on both platforms).

ifaddr — selects its BSD-vs-Linux sockaddr ctypes layout via platform.system() == "Darwin"; the flet iOS runtime reports "iOS" (PEP 730), so on iOS it parses with the Linux layout on a Darwin ABI and get_adapters() silently returns an empty list, which breaks Zeroconf() ("No interfaces to listen on"). One patch extends the check to iOS/iPadOS. The platform-tagged wheel outranks PyPI's py3-none-any at equal version, so the fix reaches consumers transparently. setup.py-only sdist → requirements.build: [setuptools].

Consumer notes (usage & recommendations)

zeroconf gives your Flet app mDNS service discovery and advertising (the protocol behind AirPlay/Chromecast/HomeAssistant device discovery) — find services on the local Wi-Fi, and/or announce your app so others can find it. Fully offline: it talks only to the local network, no internet needed.

Installation

Just add zeroconf to your dependencies. On Android you'll also want pyjnius (see why below):

[project]
dependencies = ["flet", "zeroconf"]

[tool.flet.android]
dependencies = ["pyjnius"]

[tool.flet.android.permission]
"android.permission.CHANGE_WIFI_MULTICAST_STATE" = true
"android.permission.ACCESS_NETWORK_STATE" = true

[tool.flet.ios.info]
NSLocalNetworkUsageDescription = "Finds and announces services on your local network."
NSBonjourServices = ["_demo._tcp"]   # list every service type you browse

(android.permission.INTERNET is a Flet default. Both Android permissions are install-time, no runtime prompt.)

Minimal example

Registers a service and browses for that type, so it always discovers at least itself and the list is never empty:

import os, socket
import flet as ft

def main(page: ft.Page):
    page.title = "mDNS demo"
    found = ft.ListView(expand=True)
    page.add(ft.Text("Discovered _demo._tcp services:", weight=ft.FontWeight.BOLD), found)

    if page.platform == ft.PagePlatform.ANDROID:
        # Wi-Fi chipsets drop incoming multicast unless the app holds a MulticastLock —
        # without this, zeroconf can send but hears (almost) nothing. Keep the lock
        # referenced for the app's lifetime; release it when you stop discovery.
        from jnius import autoclass
        Context = autoclass("android.content.Context")
        activity = autoclass(os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME")).mActivity
        wifi = activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE)
        page.data = lock = wifi.createMulticastLock("zeroconf")
        lock.acquire()

    from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf

    def on_change(zeroconf, service_type, name, state_change, **kwargs):
        if state_change is ServiceStateChange.Added:
            found.controls.append(ft.Text(name))
            page.update()

    zc = Zeroconf()
    zc.register_service(ServiceInfo(
        "_demo._tcp.local.", "my-flet-app._demo._tcp.local.",
        addresses=[socket.inet_aton("127.0.0.1")], port=8080,
    ))
    ServiceBrowser(zc, "_demo._tcp.local.", handlers=[on_change])

ft.run(main)

To be discoverable by other devices, advertise your device's real LAN address instead of 127.0.0.1 (e.g. open a UDP socket to 8.8.8.8 and read getsockname()[0]). There's also a fully async API (zeroconf.asyncio.AsyncZeroconf) if you prefer it inside Flet's async apps.

Android notes

  • The MulticastLock + CHANGE_WIFI_MULTICAST_STATE combo above is not optional for discovery: skipping it doesn't raise — packets just never arrive on most devices, and "discovery finds nothing" is the only symptom.
  • Emulators don't pass real LAN multicast (self-discovery as in the example works; discovering other machines needs a physical phone on real Wi-Fi).
  • Long-running background discovery isn't dependable — some devices tighten multicast filtering when the screen is off even with the lock held. Foreground use is the supported scenario. The lock also has a battery cost; release it when you stop browsing.

iOS notes

  • Works out of the box on the simulator (which is also why simulator success proves nothing about devices — the simulator doesn't enforce iOS network privacy).
  • On physical devices, iOS 14+ requires Apple's restricted com.apple.developer.networking.multicast entitlement for the raw multicast sockets python-zeroconf uses (requested from Apple per developer account); without it, sends fail with OSError: [Errno 65] No route to host. On top of that, the entitlement must be added to the app's signing — flet build currently has no way to add iOS entitlements, so you'd need a forked build template or post-build Xcode edits, plus the capability on your provisioning profile. The Info.plist keys above (settable via [tool.flet.ios.info]) cover the user-consent prompt, but they do not replace the entitlement.
  • Practical recommendation: treat zeroconf as the Android + desktop + iOS-simulator solution. For discovery on iOS devices, use the OS Bonjour APIs instead (e.g. NSNetServiceBrowser via pyobjus, or a Flutter plugin) — those need only the two Info.plist keys and no special entitlement.

Pure-Python interface enumerator (ctypes getifaddrs), the sole runtime dep
of zeroconf. Needs a recipe despite being pure Python because upstream picks
the BSD-vs-Linux sockaddr ctypes layout via platform.system() == "Darwin",
and Flet's iOS runtime reports "iOS" (PEP 730) — the Linux layout on a
Darwin ABI parses zero adapters, so get_adapters() returns an empty list and
any consumer (zeroconf: 'RuntimeError: No interfaces to listen on') breaks
on iOS. One patch extends the check to iOS/iPadOS; no upstream fix exists.
setup.py-only sdist, so requirements.build seeds setuptools. The platform-
tagged wheel outranks PyPI's py3-none-any at equal version, delivering the
fix transparently. On-device: iOS sim 2/2; android exercises the unchanged
Linux path (validated transitively via the zeroconf recipe-tester run).
mDNS/DNS-SD (flet-dev/flet discussion #6738). Cython-accelerated pure-Python
package: the 18 accelerator modules are compiled from the runtime .py files
themselves (pure-Python-mode Cython, plain self-contained C, no external
libs). First poetry-core-backend recipe in the repo — PythonPackageBuilder
handles it unchanged. Upstream's build swallows extension-compile errors even
with REQUIRE_CYTHON set, so a broken cross-compile would silently ship a
pure-Python wheel; the recipe sets REQUIRE_CYTHON=1 plus a 2-line fail-loud
patch, and the test suite asserts the compiled modules are real extensions
(including zeroconf/_services/__init__ — a native subpackage __init__, which
serious-python loads fine on both platforms). Requires-Dist: ifaddr, shipped
as a sibling recipe (iOS sockaddr-layout fix). On-device recipe-tester: 6/6
EXIT 0 on android arm64-v8a emulator AND iOS simulator, including a real
in-process register+browse loopback discovery round-trip.

[skip ci]: runtime chain — zeroconf's mobile test needs the patched ifaddr
wheel in its find-links (else pip silently pulls unpatched PyPI ifaddr and
the iOS leg fails); dispatched instead with prebuild_recipes=ifaddr.
Same Python set as 20260720 (3.12.13 / 3.13.14 / 3.14.6, same ABIs) — the
delta is Apple signing/provenance hardening (provider-signed XCFrameworks,
signed inner .framework bundles, OpenSSL privacy manifest for _ssl/_hashlib,
Xcode build-provenance keys in framework Info.plists) and dart_bridge 1.7.1.

Local note: the downloads/ tarball cache and extracted support trees are
keyed by Python version, not release — a same-version release bump silently
reuses stale trees, so purge downloads/python-*-mobile-forge-* (tarballs +
support dirs) and re-run setup.sh. Also source setup.sh from bash: zsh's
no-word-splitting makes the per-ABI android support check false-fail.

Validated locally: zeroconf iphonesimulator:arm64 + android:arm64-v8a clean
rebuilds against the fresh 20260730 trees.
@ndonkoHenri
ndonkoHenri merged commit 55b267e into main Aug 1, 2026
20 of 27 checks passed
@ndonkoHenri
ndonkoHenri deleted the zeroconf branch August 1, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant