From 11ab02859fcbf063a8c8abb0ffd769fd6e3b13cd Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 1 Jul 2026 12:02:10 +0200 Subject: [PATCH] fix(darwin): framework-ize ctypes .dylib libs so they load on the iOS simulator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The darwin site-packages sync only converted Python C-extension *.so files into device+simulator xcframeworks (dylib_ext=so); plain *.dylib shared libs (the kind a ctypes wrapper dlopens — e.g. llama-cpp-python's libllama + libggml*) were skipped and shipped straight from the archs[0]=iphoneos DEVICE slice. On the simulator that dylib then fails to dlopen: mach-o file (...libllama.dylib), but incompatible platform (have 'iOS', need 'iOS-simulator') C-extensions pass because their .so is framework-ized into a fat (device + simulator) xcframework and Xcode extracts the right per-slice binary; .dylib libs never got that treatment. Teach create_xcframework_from_dylibs to take the file extension as a parameter and iterate both `so` and `dylib` in sync_site_packages.sh, so .dylib libs also become a device+fat-simulator xcframework + a .fwork pointer, exactly like .so. For .dylib, PRESERVE the original install-name (gate the `install_name_tool -id` rewrite on ext=so). Multi-lib ctypes packages — libllama depends on @rpath/libggml*.dylib — then resolve their siblings: the wrapper preloads the dependency libs with RTLD_GLOBAL and dyld matches them by their preserved install-name. The .so path is untouched (ext=so keeps the -id rewrite), so C-extensions (numpy, stdlib) are byte-for-byte identical. Guards: `-type f` skips SONAME symlink chains; conversion is skipped when a lib has no simulator slice (avoids an empty `lipo`), leaving it as-is. Tested with llama-cpp-python on the iOS simulator: import + native ctypes calls + real GGUF inference all pass (previously failed with the platform mismatch). numpy (.so) unchanged; the device xcframework slice was validated statically (platform 2, install-names preserved, inter-lib deps intact). --- .../darwin/sync_site_packages.sh | 12 +++++++- .../darwin/xcframework_utils.sh | 29 ++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/serious_python_darwin/darwin/sync_site_packages.sh b/src/serious_python_darwin/darwin/sync_site_packages.sh index 5ca55ea9..35ee734c 100755 --- a/src/serious_python_darwin/darwin/sync_site_packages.sh +++ b/src/serious_python_darwin/darwin/sync_site_packages.sh @@ -33,16 +33,26 @@ if [[ -n "$SERIOUS_PYTHON_SITE_PACKAGES" && -d "$SERIOUS_PYTHON_SITE_PACKAGES" ] cp -R $SERIOUS_PYTHON_SITE_PACKAGES/* $tmp_dir echo "Converting dylibs to xcframeworks..." - find "$tmp_dir/${archs[0]}" -name "*.$dylib_ext" | while read full_dylib; do + # Process BOTH .so (Python C-extensions) and .dylib (ctypes-loaded shared + # libs, e.g. llama-cpp-python's libllama/libggml*). Previously only *.so + # was framework-ized; *.dylib fell through and shipped the archs[0]=iphoneos + # DEVICE build, so it failed to dlopen on the simulator. + for _sp_ext in so dylib; do + # -type f: skip SONAME symlinks (e.g. libfoo.dylib -> libfoo.1.dylib); + # only real Mach-O files are converted. Recipes should ship unversioned + # shared libs (as the flet-lib* recipes already do). + find "$tmp_dir/${archs[0]}" -name "*.$_sp_ext" -type f | while read full_dylib; do dylib_relative_path=${full_dylib#$tmp_dir/${archs[0]}/} create_xcframework_from_dylibs \ "$tmp_dir/${archs[0]}" \ "$tmp_dir/${archs[1]}" \ "$tmp_dir/${archs[2]}" \ $dylib_relative_path \ + "$_sp_ext" \ "Frameworks/serious_python_darwin.framework/python.bundle/site-packages" \ $dist/site-xcframeworks done + done rm -rf $dist/site-packages mkdir -p $dist/site-packages diff --git a/src/serious_python_darwin/darwin/xcframework_utils.sh b/src/serious_python_darwin/darwin/xcframework_utils.sh index 87447b4a..49a7b621 100644 --- a/src/serious_python_darwin/darwin/xcframework_utils.sh +++ b/src/serious_python_darwin/darwin/xcframework_utils.sh @@ -45,8 +45,9 @@ create_xcframework_from_dylibs() { simulator_arm64_dir=$2 simulator_x86_64_dir=$3 dylib_relative_path=$4 - origin_prefix=$5 - out_dir=$6 + ext=${5:-$dylib_ext} + origin_prefix=$6 + out_dir=$7 dylib_tmp_dir=$(mktemp -d) pushd -- "${dylib_tmp_dir}" >/dev/null @@ -60,12 +61,24 @@ create_xcframework_from_dylibs() { done framework_identifier=${framework_identifier:-framework} + # If neither simulator slice has this lib, leave the file untouched rather + # than fail `lipo` on an empty input (e.g. a device-only artifact). It then + # ships as-is in the flat base, exactly as before this conversion existed. + if [ -z "$(find "$simulator_arm64_dir" "$simulator_x86_64_dir" \ + \( -path "*/$dylib_without_ext.*.$ext" -o -path "*/$dylib_without_ext.$ext" \) \ + -type f 2>/dev/null | head -1)" ]; then + echo " no simulator slice for $dylib_relative_path; leaving as-is" + popd >/dev/null + rm -rf "${dylib_tmp_dir}" >/dev/null + return + fi + # creating "iphoneos" framework fd=iphoneos/$framework.framework mkdir -p $fd mv "$iphone_dir/$dylib_relative_path" $fd/$framework echo "Frameworks/$framework.framework/$framework" > "$iphone_dir/$dylib_without_ext.fwork" - install_name_tool -id @rpath/$framework.framework/$framework $fd/$framework + if [ "$ext" = "so" ]; then install_name_tool -id @rpath/$framework.framework/$framework $fd/$framework; fi create_plist $framework "org.python.$framework_identifier" $fd/Info.plist echo "$origin_prefix/$dylib_without_ext.fwork" > $fd/$framework.origin @@ -73,12 +86,12 @@ create_xcframework_from_dylibs() { fd=iphonesimulator/$framework.framework mkdir -p $fd lipo -create \ - $(find "$simulator_arm64_dir" -path "$simulator_arm64_dir/$dylib_without_ext.*.$dylib_ext" -o -path "$simulator_arm64_dir/$dylib_without_ext.$dylib_ext") \ - $(find "$simulator_x86_64_dir" -path "$simulator_x86_64_dir/$dylib_without_ext.*.$dylib_ext" -o -path "$simulator_x86_64_dir/$dylib_without_ext.$dylib_ext") \ + $(find "$simulator_arm64_dir" -path "$simulator_arm64_dir/$dylib_without_ext.*.$ext" -o -path "$simulator_arm64_dir/$dylib_without_ext.$ext") \ + $(find "$simulator_x86_64_dir" -path "$simulator_x86_64_dir/$dylib_without_ext.*.$ext" -o -path "$simulator_x86_64_dir/$dylib_without_ext.$ext") \ -output $fd/$framework - find "$simulator_arm64_dir" -path "$simulator_arm64_dir/$dylib_without_ext.*.$dylib_ext" -o -path "$simulator_arm64_dir/$dylib_without_ext.$dylib_ext" -delete - find "$simulator_x86_64_dir" -path "$simulator_x86_64_dir/$dylib_without_ext.*.$dylib_ext" -o -path "$simulator_x86_64_dir/$dylib_without_ext.$dylib_ext" -delete - install_name_tool -id @rpath/$framework.framework/$framework $fd/$framework + find "$simulator_arm64_dir" -path "$simulator_arm64_dir/$dylib_without_ext.*.$ext" -o -path "$simulator_arm64_dir/$dylib_without_ext.$ext" -delete + find "$simulator_x86_64_dir" -path "$simulator_x86_64_dir/$dylib_without_ext.*.$ext" -o -path "$simulator_x86_64_dir/$dylib_without_ext.$ext" -delete + if [ "$ext" = "so" ]; then install_name_tool -id @rpath/$framework.framework/$framework $fd/$framework; fi create_plist $framework "org.python.$framework_identifier" $fd/Info.plist echo "$origin_prefix/$dylib_without_ext.fwork" > $fd/$framework.origin