Skip to content

recipe: pycares 5.0.1 - #118

Merged
ndonkoHenri merged 4 commits into
mainfrom
pycares
Aug 31, 2026
Merged

recipe: pycares 5.0.1#118
ndonkoHenri merged 4 commits into
mainfrom
pycares

Conversation

@ndonkoHenri

Copy link
Copy Markdown

Adds a recipe for pycares 5.0.1 — Python bindings for c-ares, the asynchronous DNS resolver that aiodns is built on. Requested in flet#6805.

socket.getaddrinfo blocks, and asyncio only moves that block onto a thread-pool worker — so resolution is bounded by the pool, not the network. c-ares speaks DNS itself and keeps hundreds of lookups in flight, which is the reason to carry it on a phone. aiodns needs no recipe of its own (pure Python on PyPI) and cffi is already published, so this one recipe is the whole ask.

Recipe shape

A plain setuptools sdist with one CFFI (API-mode) extension and one patch — but a shape this repo hasn't had before. pycares' setup.py builds the vendored deps/c-ares (1.34.6) with its own subprocess CMake call inside build_ext, then links the resulting libcares.a through extra_objects. No CMake build backend is involved, so CMAKE_ARGS does nothing — the argument list is a literal in setup.py, configured for the build host.

mobile.patch makes that list extensible, in one line of substance:

cmake_args.extend(shlex.split(os.environ.get('FORGE_CMAKE_ARGS', '')))

Appended after upstream's own platform block so the recipe's toolchain arguments win — repeated -D on a cmake command line is last-one-wins, which is also how upstream's -DCMAKE_OSX_DEPLOYMENT_TARGET=10.12 gets overridden without editing its line.

Getting this wrong fails green. A host-configured libcares.a still links whenever host and target arch agree (macOS arm64 objects go into an ios_arm64 extension without complaint), and the configure-time probes answer for macOS. Two of those probes are load-bearing and were verified in the build logs: CARES_THREADS (import pycares raises RuntimeError outright if ares_threadsafety() is false) and HAVE___SYSTEM_PROPERTY_GET / epoll / kqueue for the event thread.

That is the entire adaptation. Everything else falls out because sys.platform is android/ios under the crossenv, so none of setup.py's darwin/linux/win32 branches match — which is exactly right: no -lrt on Android (bionic has no librt), no macOS deployment target on iOS. No extra link flags on either platform; the iOS libcares.a resolves entirely against libSystem and Android's against bionic.

Validation

6/6 slices built clean first try. CI green across the full matrix — 18 wheels (3.12/3.13/3.14 × 6 slices) — with the on-device console.log artifacts showing 6 passed / EXIT 0 on both platforms. Also run locally on an API-34 arm64 emulator and an iPhone 16 Pro simulator.

Wheel hygiene per slice: correct Machine per ABI, every Android LOAD segment aligned 0x4000, DT_NEEDED limited to libc/libm/libdl plus libpython, iOS LC_BUILD_VERSION platform 2 on device and 7 on the simulators, otool -L showing only libSystem and the Python framework. ~116–143 KB compressed per slice.

The recipe tests are deliberately network-free, so they cannot catch a resolver that builds but can't reach anything. The dns-lookup example was run against a real network on both platforms to close that gap — and that is what surfaced the consumer note below.

Changes

  • recipes/pycares/meta.yaml, one patch, 6 on-device tests, README.md, and a runnable example.
  • .claude/skills/ — the new recipe shape in new-mobile-recipe, plus two forge-error-catalogue entries (the green-but-host-configured build class above, and the Android runtime class below). Separable; drop the commit if you'd rather keep skills out of a recipe PR.

Adds a recipe for [pycares](https://github.com/saghul/pycares) 5.0.1 — Python
bindings for [c-ares](https://c-ares.org/), the asynchronous DNS resolver behind
[aiodns](https://github.com/aio-libs/aiodns). Requested in
[flet#6805](flet-dev/flet#6805).

- [Docs](recipes/pycares/README.md)
- [Example](recipes/pycares/examples/dns-lookup)

`socket.getaddrinfo` blocks, and asyncio only moves that block onto a thread-pool
worker. c-ares speaks DNS itself, so lookups are genuinely concurrent — which is the
whole reason to carry it on a phone. aiodns is pure Python and needs no recipe of its
own; cffi is already published.

## Recipe shape

A plain setuptools sdist with one CFFI (API-mode) extension, one patch. pycares'
`setup.py` builds the vendored `deps/c-ares` (1.34.6) with CMake and links the
resulting `libcares.a` into the extension through `extra_objects`, but its
`cmake_args` list is hardcoded and configures for the build host — an unpatched cross
build folds a macOS static library into the mobile `.so`. `mobile.patch` appends
`shlex.split(os.environ['FORGE_CMAKE_ARGS'])` last, so the recipe's toolchain args win,
including over upstream's `-DCMAKE_OSX_DEPLOYMENT_TARGET=10.12`.

That is the whole adaptation. `sys.platform` is `android`/`ios` under the crossenv, so
none of `setup.py`'s `darwin`/`linux`/`win32` branches match and the things that would
have needed patching never fire: no `-lrt` on Android (bionic has no librt), no macOS
deployment target on iOS. No extra link flags on either platform — iOS `libcares.a`
resolves entirely against libSystem, Android's against bionic.

Two configure-time facts the wheel depends on, both verified in the build logs:
`CARES_THREADS` (`import pycares` raises outright if `ares_threadsafety()` is false)
and the target-side probes — `HAVE___SYSTEM_PROPERTY_GET` on Android, kqueue/epoll for
the event thread. Configured against the host they yield a green wheel that dies at
`Channel()`.

## Validation

6/6 slices built clean first try. On-device 6/6 tests EXIT 0 on both an API-34 arm64
emulator and an iPhone 16 Pro simulator. Wheel hygiene checked per slice: correct
`Machine` per ABI, all Android `LOAD` segments aligned `0x4000`, `DT_NEEDED` limited to
`libc`/`libm`/`libdl` + `libpython`, iOS `LC_BUILD_VERSION` platform 2 on device and 7
on the simulators.

The tests are network-free by design, so the `dns-lookup` example was also run against
a real network on both platforms — that is what surfaced the one thing consumers must
know:

**On Android c-ares finds no system nameservers and every lookup fails until the app
supplies its own.** Android 8 removed the `net.dns*` properties and the replacement is
Java-only, reachable solely through `ares_library_init_android()`, which pycares does
not expose. c-ares then falls back to `127.0.0.1:53` rather than failing loudly. On the
emulator: `auto-discovered servers: ['127.0.0.1:53']`, system DNS →
`DNSError: (11, 'Could not contact DNS servers')`, explicit `1.1.1.1`/`8.8.8.8` →
resolves. iOS is unaffected — c-ares reads the system configuration and both paths
work. The README leads with this; the example shows both outcomes on screen.
…ig APIs [skip ci]

From the pycares recipe.

`new-mobile-recipe`: a shape row + deep-dive for an sdist that vendors a C library
and builds it with its OWN cmake call inside `build_ext` — `CMAKE_ARGS` does nothing
there, so the recipe patches in a `FORGE_CMAKE_ARGS` extend. Also extends the
platform-predicate trap to `sys.platform` (crossenv sets it to `android`/`ios`), with
the note that matching nothing is sometimes the correct outcome.

`forge-error-catalogue`: the build-time entry for that shape — it fails GREEN, because
a host-configured vendored lib still links whenever host and target arch agree — plus
a runtime entry for the Android class it belongs to: a library that reads system
configuration through a Java-only API gets nothing under Flet, and seeds a default
rather than erroring. c-ares is the worked example (127.0.0.1:53, iOS unaffected).

Also corrects the aiodns wheel size in the pycares README.
The recipe README went from 5 links to 57 and the example README from 5 to 8 —
every API, spec, platform behaviour and file it names now points at where a reader
can follow it up: CPython (`socket.getaddrinfo`, the asyncio loop methods,
`asyncio.gather`, the stdlib IDNA codec), c-ares man pages
(`ares_library_init_android`, `ares_search`, `ares_getaddrinfo`, `ares_strerror`,
`ares_inet_pton`, `ares_threadsafety`), pycares' own `docs/channel.rst`, aiodns' API
section, CFFI's API-mode and `ffi.cdef` docs, Android references
(`ConnectivityManager`, `LinkProperties.getDnsServers()`, `INTERNET`, the Android 8
behaviour changes), Apple's `SCDynamicStore` / `NSLocalNetworkUsageDescription` /
TN3179, the resolv.conf and epoll/kqueue man pages, Flet's `page.run_thread` and
Android permissions docs, the three public resolvers named, and the recipe's own
`meta.yaml` / `mobile.patch` / tests / example.

Also fixes a link that was already broken —
`flet.dev/docs/cookbook/native-python-apis` 404s; pyjnius now points at its own
docs — and puts the `getDnsServers()` URL in a pointy-bracket destination, since the
parentheses would otherwise terminate the Markdown link early.

All 46 external URLs verified 200; both files parsed with a CommonMark parser and
every relative target checked to exist.
@ndonkoHenri
ndonkoHenri merged commit 5482d5e into main Aug 31, 2026
25 of 26 checks passed
@ndonkoHenri
ndonkoHenri deleted the pycares branch August 31, 2026 18:37
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