Skip to content

Mobile packaging fixes: iOS interdependent-dylib frameworks + Android native-__init__ packages#232

Merged
FeodorFitsner merged 4 commits into
mainfrom
fix/soref-package-init
Jul 13, 2026
Merged

Mobile packaging fixes: iOS interdependent-dylib frameworks + Android native-__init__ packages#232
FeodorFitsner merged 4 commits into
mainfrom
fix/soref-package-init

Conversation

@ndonkoHenri

Copy link
Copy Markdown
Collaborator

Two independent Flet 0.86 mobile-packaging fixes, surfaced while building native-heavy wheels (pyarrow, llama-cpp-python, apsw).

1. iOS — reconcile framework install-names for interdependent dylibs · fixes #223

create_xcframework_from_dylibs renames each bundled .so/.dylib to a framework named by its dotted relative path (opt/lib/libarrow.dylibopt.lib.libarrow.framework/opt.lib.libarrow) but leaves every Mach-O install-id and interdependent @rpath reference at its original bare name (@rpath/libarrow.dylib) — it only runs install_name_tool -id, and only for .so. Each framework is a Package.swift binaryTarget the plugin links at launch, so dyld can't resolve @rpath/libarrow.dylib (it looks for Frameworks/libarrow.dylib, which doesn't exist) and the app crashes before Python starts:

dyld: Library not loaded: @rpath/libarrow.dylib
      Referenced from: <app>.debug.dylib

Adds reconcile_framework_install_names() (xcframework_utils.sh), called from sync_site_packages.sh after the conversion loop: it sets each framework binary's own install-id to @rpath/<fw>.framework/<fw> and rewrites every dependency that pointed at a sibling's old id to that framework path, then re-signs. The Python/stdlib xcframeworks are already correct and excluded.

Affects any package bundling a chain of interdependent dylibs — pyarrow (libarrowlibarrow_computelibarrow_python + the pyarrow C-extensions), llama-cpp-python (libggml-baselibggml-cpulibggmllibllama).

2. Android — resolve a package whose __init__ IS a native extension

_SorefFinder only probed <dotted>.soref. A package whose __init__ is itself the extension — apsw ships apsw/__init__.<abi>.so, import name apsw — has its marker at <dotted>/__init__.soref, so find_spec returned None, the synthesized empty apsw/__init__.py won, and import apsw yielded an empty module (AttributeError: module 'apsw' has no attribute 'Connection'). find_spec now falls back to <dotted>/__init__.soref, loads the extension under the correct top-level name via ExtensionFileLoader, and marks it a package with submodule_search_locations so pure-Python submodules (apsw.ext, …) still resolve.

Testing

Verified through the mobile-forge recipe-tester (Flet 0.86, py3.12), pinning
serious_python to this branch via the app's Flutter dependency_overrides:

_SorefFinder only probed "<dotted>.soref" for a relocated native module, so a
package whose __init__ is itself the extension (e.g. apsw ships
apsw/__init__.<abi>.so, dotted import name `apsw`) was never resolved: the
marker for it is written at "<dotted>/__init__.soref", not "<dotted>.soref".
find_spec returned None, the synthesized empty apsw/__init__.py won, and
`import apsw` yielded an empty module -> AttributeError on apsw.Connection /
apsw.apswversion().

find_spec now falls back to "<dotted>/__init__.soref" when the plain marker
misses, loads the extension under the correct top-level name via
ExtensionFileLoader (its PyInit_<name> matches, e.g. PyInit_apsw), and marks the
result a package with submodule_search_locations pointing at the package dir in
the winning sys.path entry (sitepackages.zip or an extract.zip dir) so pure-
Python submodules (apsw.ext, apsw._unicode, ...) resolve normally. _read_marker
now also returns the winning entry for that purpose.

Verified on-device (Android arm64, Flet 0.86): apsw 3.53.2.0 imports and runs a
full in-memory SQLite CREATE/INSERT/SELECT round-trip (2/2 recipe tests pass);
previously both failed with the empty-__init__ AttributeError.
…223)

After sync_site_packages framework-izes each site-package .so/.dylib into a
framework named by its dotted relative path (opt/lib/libarrow.dylib ->
opt.lib.libarrow.framework/opt.lib.libarrow), the Mach-O install-ids and every
interdependent @rpath reference were left at their original bare names
(@rpath/libarrow.dylib). Because each framework is a Package.swift binaryTarget
the plugin depends on, dyld links them all at launch, and a bare
@rpath/libarrow.dylib resolves to Frameworks/libarrow.dylib -- which does not
exist -- so the app crashes before Python starts (e.g. pyarrow, llama-cpp).

Add reconcile_framework_install_names(): after the conversion loop, set each
framework binary's own install-id to @rpath/<fw>.framework/<fw> and rewrite
every dep that pointed at a sibling's old id to that sibling's framework path,
then re-sign. The python/stdlib xcframeworks are already correct and excluded.

Verified locally: pyarrow 24.0.0 on iOS simulator now launches and passes 4/4
tests (was a dyld launch crash: Library not loaded @rpath/libarrow.dylib).
…ce ids)

Two robustness fixes to the #223 reconcile pass (both from code review):

1. Stop swallowing real failures. The install_name_tool/-id/-change and codesign
   calls used `2>/dev/null || true`, which also hid genuine failures — most
   importantly install_name_tool's 'larger updated load commands do not fit'
   (a Mach-O with no header space to grow a load command). A swallowed -change
   leaves a bare @rpath ref and reproduces the exact dyld launch crash this pass
   exists to prevent, silently. Now: -id, a *present-dep* -change (absent deps
   are still a rc-0 no-op), and codesign failures are fatal — the function
   returns non-zero with a clear message, and sync_site_packages.sh propagates it
   (`|| exit 1`) so `flet build` fails loudly instead of shipping a broken app.
   stderr is captured and only printed on error, so the expected
   'will invalidate the code signature' warning stays off the build log.

2. Record every slice's old install-id, not just the first. Pass 1 read oldid
   only from the first slice; a lib whose slices carry divergent install names
   (e.g. an arch-specific absolute build path instead of an @rpath id) would
   leave the other slices' deps unrewritten. Now each distinct old id is mapped
   (deduped) to the same new framework id.

Verified: happy path (padded dylibs) + the real llama ggml->llama chain both
reconcile to rc 0 with no bare refs; a divergent-slice-id pair rewrites both
slices; an unpadded binary now fails loudly (rc 1) instead of silently.
…o the unreleased 4.3.2

4.3.2 is not yet released, so these packaging fixes ship in it rather than a new
version: darwin gets the interdependent-dylib framework reconcile (#223) + its
fail-loud/all-slice hardening; android gets the native-__init__ soref resolution
(apsw); the main package aggregates both. Versions unchanged (already 4.3.2).
@FeodorFitsner FeodorFitsner merged commit 884fb94 into main Jul 13, 2026
36 of 58 checks passed
@FeodorFitsner FeodorFitsner deleted the fix/soref-package-init branch July 13, 2026 18:09
@ndonkoHenri ndonkoHenri restored the fix/soref-package-init branch July 13, 2026 19:01
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.

2 participants