Skip to content

xcode 引入scipy,真机调试scipy依赖的文件找不到 #2

Open

Description

@baixiaochunbudanchun

您好,我在xcode OC语言,集成python环境,然后调用我自己的脚本,我的脚本依赖了scipy使用 from scipy.signal import butter, sosfiltfilt, find_peaks, savgol_filter
from scipy.signal.windows import gaussian 这几个依赖函数,然后报如下错误

Traceback (most recent call last):
File "", line 1, in
File "/private/var/containers/Bundle/Application/A3F77FC6-80B2-48D2-8EC7-739316232F85/hLifeKup.app/complete_preprocessing_pipeline.py", line 73, in
from scipy.signal import butter, sosfiltfilt, find_peaks, savgol_filter
File "/private/var/containers/Bundle/Application/A3F77FC6-80B2-48D2-8EC7-739316232F85/hLifeKup.app/python-ios-lib_SciPy.bundle/scipy/signal/init.py", line 293, in
from . import _sigtools, windows
File "/private/var/containers/Bundle/Application/A3F77FC6-80B2-48D2-8EC7-739316232F85/hLifeKup.app/python-ios-lib_SciPy.bundle/scipy/signal/windows/init.py", line 42, in
from ._windows import *
File "/private/var/containers/Bundle/Application/A3F77FC6-80B2-48D2-8EC7-739316232F85/hLifeKup.app/python-ios-lib_SciPy.bundle/scipy/signal/windows/_windows.py", line 7, in
from scipy import linalg, special, fft as sp_fft
File "/private/var/containers/Bundle/Application/A3F77FC6-80B2-48D2-8EC7-739316232F85/hLifeKup.app/python-ios-lib_SciPy.bundle/scipy/init.py", line 134, in getattr
return _importlib.import_module(f'scipy.{name}')
~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "/private/var/containers/Bundle/Application/A3F77FC6-80B2-48D2-8EC7-739316232F85/hLifeKup.app/python-stdlib/importlib/init.py", line 88, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/containers/Bundle/Application/A3F77FC6-80B2-48D2-8EC7-739316232F85/hLifeKup.app/python-ios-lib_SciPy.bundle/scipy/linalg/init.py", line 221, in
from ._decomp_update import *
File "scipy/linalg/_decomp_update.pyx", line 1, in init scipy.linalg._decomp_update
ImportError: dlopen(/private/var/containers/Bundle/Application/A3F77FC6-80B2-48D2-8EC7-739316232F85/hLifeKup.app/python-ios-lib_SciPy.bundle/scipy/linalg/cython_blas.cpython-314-iphoneos.so, 0x0002): symbol not found in flat namespace 'dcabs1'
❌w模块导入失败,
Numpy 版本: 2.3.5
Scipy 版本: 1.15.0

我是按照项目的使用说明集成的,我该怎么解决这个问题?

Activity

  1. baixiaochunbudanchun commented on May 28, 2026

    @baixiaochunbudanchun
    Author

    scipy.linalg/ scipy.signal的 .so扩展(cython_blas, cython_lapack)在 iOS 真机运行时报 symbol not found in flat namespace 'dcabs1',原因是 BLAS 符号未静态链接进 .so,iOS 的 Accelerate BLAS 符号不公开无法在 flat namespace 解析。作者可以在构建时把 OpenBLAS 或静态 BLAS 的 dcabs1, _lsame_等符号链接进这些 .so,或改用完整静态合并的 scipybundle。

  2. yu314-coder commented on May 31, 2026

    @yu314-coder
    Owner

    你好,这个问题已经定位并修复,修复已经推送到 main。

    原因

    scipy.linalg.cython_blas 引用了三个 参考 BLAS 辅助函数:dcabs1、scabs1、lsame。苹果的 Accelerate 框架提供了 BLAS/LAPACK 主体内核(dgemm、zgesv 等),但没有导出 dcabs1 / scabs1;而 cython_blas 是在 flat namespace(平面命名空间) 里查找它们的,所以真机上 dyld 一旦找不到 dcabs1 就崩溃:

    symbol not found in flat namespace '_dcabs1_'
    

    (lsame 其实在 Accelerate 里,是 two-level 绑定,没问题;真正缺的只有 dcabs1 / scabs1。)

    最简单的解决办法(推荐)

    在你自己的 App target 里加入一个很小的 C 文件,把缺失的符号放进主程序,flat-namespace 查找就能解析到——不用改 scipy,也不用额外的 dylib:

    // blas_compat_stubs.c
    #include <math.h>
    double dcabs1_(const double *z) { return fabs(z[0]) + fabs(z[1]); }
    float  scabs1_(const float  *c) { return fabsf(c[0]) + fabsf(c[1]); }
    int lsame_(const char *ca, const char *cb, long la, long lb) {
        (void)la; (void)lb;
        int a=(unsigned char)ca[0], b=(unsigned char)cb[0];
        if (a>='a'&&a<='z') a-=32; if (b>='a'&&b<='z') b-=32; return a==b;
    }

    把 blas_compat_stubs.c 加进 Xcode 的 Build Phases → Compile Sources,重新编译,import scipy.signal / scipy.linalg 就能正常加载了。

    仓库里已经带上的修复

    为了让打包好的 scipy「开箱即用」,main 上现在也加了:

    • fortran/blas_compat_stubs.c — 这三个辅助函数的源码;
    • Frameworks/scipy_aux/libscipy_blas_stubs.dylib — 预编译好的(arm64-ios,ad-hoc 签名,@rpath);
    • 给 cython_blas*.so 加了 LC_LOAD_DYLIB,运行时解析这个 dylib;
    • scripts/appstore/build-blas-stubs.sh — 每次重建 scipy 后可重新生成;
    • docs/scipy_ios_dcabs1_fix.md 有详细说明,test_scipy_blas_fix.py 可在真机上验证。

    如果你是直接嵌入这个包,记得把 libscipy_blas_stubs.dylib 拷进 App 的 Frameworks/ 并保证有 @rpath 能找到它;或者直接用上面「最简单办法」,连 dylib 都不用。

    我在真机(iPad / iOS 26)上验证过:复数 linalg.solve、eigvals,以及 butter→sosfiltfilt→savgol_filter→find_peaks 信号管线都通过了。麻烦你试一下,确认是否解决,谢谢!


    English: Accelerate doesn't export the reference-BLAS helpers dcabs1/scabs1, and cython_blas looks them up in the flat namespace → dyld aborts on device. Easiest fix: add the tiny blas_compat_stubs.c above to your app target (Build Phases → Compile Sources) so the symbols live in your main binary — no scipy patching, no extra dylib. main now also bundles libscipy_blas_stubs.dylib + an LC_LOAD_DYLIB on cython_blas*.so so the packaged scipy works out of the box (see docs/scipy_ios_dcabs1_fix.md; verify on-device with test_scipy_blas_fix.py). Please give it a try and let me know if it resolves the crash.

  3. baixiaochunbudanchun commented on Jun 1, 2026

    @baixiaochunbudanchun
    Author

    作者你好,非常感谢你给的回复及解决方案,我通过导入libfortran_io_stubs.dylib,libsf_error_state.dylib,libscipy_blas_stubs.dylib三个文件,在真机上能够将脚本模块导入成功,并获取对应的函数。
    现在我又遇到提交商店代码审核的问题,按照你的readme文档操作,执行了wrap-loose-dylibs脚本,但是仍然会报Upload-fail-with-errors.txt文档中的错误,
    Run-Script-build-phase.txt 是我按照5. Boot Python and add the bundles to sys.path进行的项目配置
    Run-Wrap-loose-dylibs.txt是我按照App Store distribution 指导进行的配置,
    wrap-loose-dylibs-result.txt 是编译之后脚本执行的结果,
    我的项目只引用了numpy和scipy 两个库,请你在帮我看看什么原因导致商店审核不通过

    Image Image [Run-Script-build-phase.txt](https://github.com/user-attachments/files/28462296/Run-Script-build-phase.txt) [Run-Wrap-loose-dylibs.txt](https://github.com/user-attachments/files/28462297/Run-Wrap-loose-dylibs.txt) [Upload-fail-with-errors.txt](https://github.com/user-attachments/files/28462298/Upload-fail-with-errors.txt) [wrap-loose-dylibs-result.txt](https://github.com/user-attachments/files/28462299/wrap-loose-dylibs-result.txt)
  4. baixiaochunbudanchun commented on Jun 1, 2026

    @baixiaochunbudanchun
    Author
    Image
  5. yu314-coder commented on Jun 1, 2026

    @yu314-coder
    Owner

    很高兴 dcabs1 的问题已经解决,scipy 在真机上能正常导入了 👍

    关于上传 App Store 失败 —— 先说明一下:你附的四个 .txt(Upload-fail-with-errors.txt 等)通过邮件回复发上来时变成了失效链接。GitHub 的邮件回复网关只保留图片附件,会把 .txt 之类的文件丢掉,所以它们点开都是 404(对我、对你都一样)。以后如果要我看日志,麻烦直接把文件拖进网页评论框(不要用邮件回复),链接才有效。

    不过你的两张截图已经足够定位问题了:

    ① General 标签 —— 三个桩 dylib 都设成了 Embed & Sign:
    libfortran_io_stubs.dylib、libscipy_blas_stubs.dylib、libsf_error_state.dylib
    也就是以松散 dylib(loose .dylib)的形式打进 App 包里。

    ② Build Phases 的执行顺序:

    Compile Sources (24)
    Link Binary With Libraries (6)
    Copy Bundle Resources (19)
    Sign Python Binary Modules
    Run Wrap-loose-dylibs        ← 脚本在这里执行
    Embed Frameworks (4 items)   ← 但那三个 dylib 是在这之后才被嵌入的
    

    这个顺序就是根因。 Run Wrap-loose-dylibs 排在 Embed Frameworks 前面,所以脚本去扫描 Frameworks/ 时,那三个桩 dylib 还没被拷进去(Embed Frameworks 在后面才拷)。脚本把 python-ios-lib 包自带的二进制都正确打包成 framework 了,唯独漏掉了你手动加的这三个 —— 它们最终以松散、未打包的形式留在了 .app/Frameworks/ 里。Apple 的校验器就会拒绝这些松散 .dylib(ITMS-90xxx 里 "Invalid Bundle / 不支持的动态库" 那一类)。

    解决办法(拖一下顺序就行):
    在 CustomCalculator target → Build Phases,把 Run Wrap-loose-dylibs 拖到最底部,放到 Embed Frameworks 的下面,让它在 dylib 被嵌入之后再执行打包。然后重新 Archive → 上传 即可。

    README 里写的是"放在最后的隐式 code-sign 之前",这点没错;这里补一句更关键的:必须排在 Embed Frameworks 之后,否则脚本看不到刚嵌进来的 dylib。

    可选精简:把那段很小的 blas_compat_stubs.c 加进 App target(Build Phases → Compile Sources),就能把 libscipy_blas_stubs.dylib 整个去掉(少一个松散 dylib);但另外两个 libfortran_io_stubs / libsf_error_state 仍然要靠 wrap 脚本打包成 framework —— 所以调整 Build Phase 顺序才是这次的关键。

    调整后如果还报错,请把 Upload-fail-with-errors.txt 的内容直接贴在评论里(或拖进评论框),我再帮你看是具体哪一类。


    English summary: Your four .txt logs arrived as dead links — GitHub's email-reply gateway only keeps image attachments, so the .txt files were never hosted (they 404 for everyone). Please drag them into a web comment if you want me to read them next time. Your two screenshots are enough, though: the three stub dylibs (libfortran_io_stubs, libscipy_blas_stubs, libsf_error_state) are Embed & Sign (loose dylibs), and in Build Phases your Run Wrap-loose-dylibs phase runs before Embed Frameworks — so those three dylibs aren't in Frameworks/ yet when the wrap script scans, and they ship loose → Apple rejects them (ITMS-90xxx Invalid Bundle / unsupported dylib).

    Fix: in the CustomCalculator target → Build Phases, drag Run Wrap-loose-dylibs to the bottom, below Embed Frameworks, then re-Archive and upload. (Optionally add blas_compat_stubs.c to the app target to drop libscipy_blas_stubs.dylib, but the phase reorder is the real fix.) If it still fails, paste the contents of Upload-fail-with-errors.txt directly and I'll pinpoint the exact category.

  6. baixiaochunbudanchun commented on Jun 2, 2026

    @baixiaochunbudanchun
    Author

    非常感谢你的回复和指导,由于中国访问github不是很稳定,我分几步提交我遇到的问题。第一、我听取你的建议,使用blas_compat_stubs.c 加进 App target(Build Phases → Compile Sources)这种方式,但是报了如下如错误,

    Image 因此我还是使用原来的方法,我调整了Build Phases 的执行顺序:

    Copy Bundle Resources (19)
    Embed Frameworks (4 items)
    Sign Python Binary Modules
    Run Wrap-loose-dylibs
    编译时报了如下图错误

    Image

    我通过修改 wrap-loose-dylibs 脚本,解决上面问题,进行打包提交,

    #!/bin/bash

    wrap-loose-dylibs.sh — App Store fix for CodeBench / hLifeKup

    ===============================================================

    1. 将 Frameworks/ 中的松散 .dylib 包装为 .framework

    2. 重写所有二进制中对这些 dylib 的引用(包括主可执行文件、已有 framework、.so 等)

    3. 清理 App Store 会拒绝的静态库、位码、私有 API 等

    4. 重新签名所有修改过的二进制

    Build Settings 要求:ENABLE_USER_SCRIPT_SANDBOXING = NO

    ===============================================================

    set +e

    echo "=== wrap-loose-dylibs.sh starting ==="
    echo " BUILT_PRODUCTS_DIR=${BUILT_PRODUCTS_DIR:-(unset)}"
    echo " WRAPPER_NAME=${WRAPPER_NAME:-(unset)}"
    echo " EXECUTABLE_NAME=${EXECUTABLE_NAME:-(unset)}"
    echo " EXPANDED_CODE_SIGN_IDENTITY=${EXPANDED_CODE_SIGN_IDENTITY:-(unset)}"
    echo " ENABLE_USER_SCRIPT_SANDBOXING=${ENABLE_USER_SCRIPT_SANDBOXING:-(unset)}"

    确定 .app 路径(优先使用 CODESIGNING_FOLDER_PATH)

    if [ -n "${CODESIGNING_FOLDER_PATH}" ] && [ -d "${CODESIGNING_FOLDER_PATH}" ]; then
    APP="${CODESIGNING_FOLDER_PATH}"
    else
    APP="${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}"
    fi
    FW="$APP/Frameworks"
    IDENT="${EXPANDED_CODE_SIGN_IDENTITY:--}"

    if [ ! -d "$APP" ]; then
    for _alt in
    "${TARGET_BUILD_DIR}/${WRAPPER_NAME}"
    "${INSTALL_DIR}/${WRAPPER_NAME}"
    "${INSTALL_ROOT}${INSTALL_PATH}/${WRAPPER_NAME}"
    "${DSTROOT}${INSTALL_PATH}/${WRAPPER_NAME}"; do
    if [ -d "$_alt" ]; then
    APP="$_alt"
    FW="$APP/Frameworks"
    echo "wrap-loose-dylibs: using fallback APP=$APP"
    break
    fi
    done
    fi
    if [ ! -d "$APP" ]; then
    echo "wrap-loose-dylibs: ERROR APP dir missing — refusing to silently skip"
    echo " BUILT_PRODUCTS_DIR=${BUILT_PRODUCTS_DIR}"
    echo " TARGET_BUILD_DIR=${TARGET_BUILD_DIR}"
    echo " INSTALL_DIR=${INSTALL_DIR}"
    echo " DSTROOT=${DSTROOT}"
    exit 1
    fi
    if [ ! -d "$FW" ]; then
    echo "wrap-loose-dylibs: no Frameworks/ dir at $FW — Install Python phase didn't run yet"
    exit 1
    fi

    ========== 提前定义主可执行文件路径 ==========

    MAIN_BIN="$APP/$EXECUTABLE_NAME"

    ============================================

    ============================================================

    Step 0: 硬删除 App Store 明确拒绝的文件

    ============================================================

    echo "wrap-loose-dylibs: Step 0 — hard cleanup of App Store-rejected files"
    _HARD_CLEANUP_PATHS=(
    "$FW/libkernels_optimized_ios.a"
    "$FW/libexecutorch_ios.a"
    "$FW/libbackend_coreml_ios.a"
    "$FW/libbackend_xnnpack_ios.a"
    "$FW/libthreadpool_ios.a"
    "$APP/app_packages/site-packages/numpy/_core/lib/libnpymath.a"
    "$APP/app_packages/site-packages/numpy/random/lib/libnpyrandom.a"
    "$APP/app_packages/site-packages/scipy/.dylibs/libgcc_s.1.1.dylib"
    "$APP/app_packages/site-packages/scipy/.dylibs/libgfortran.5.dylib"
    "$APP/app_packages/site-packages/scipy/.dylibs/libquadmath.0.dylib"
    "$FW/libgcc_s_1_1.framework"
    "$FW/libgfortran_5.framework"
    "$FW/libquadmath_0.framework"
    "$APP/app_packages/site-packages/manimpango/_register_font.cpython-314-iphoneos.so"
    "$APP/app_packages/site-packages/manimpango/cmanimpango.cpython-314-iphoneos.so"
    "$APP/app_packages/site-packages/manimpango/enums.cpython-314-iphoneos.so"
    "$FW/site-packages.manimpango._register_font.framework"
    "$FW/site-packages.manimpango.cmanimpango.framework"
    "$FW/site-packages.manimpango.enums.framework"
    "$APP/app_packages/site-packages/psutil/_psutil_osx.abi3.so"
    "$APP/app_packages/site-packages/psutil/_psutil_osx.abi3.fwork"
    "$FW/site-packages.psutil._psutil_osx.framework"
    )
    for _f in "${_HARD_CLEANUP_PATHS[@]}"; do
    if [ -e "$_f" ]; then
    rm -rf "$_f" && echo " removed $_f"
    fi
    done
    echo " APP=$APP ($(du -sh "$APP" 2>/dev/null | awk '{print $1}'))"
    echo " FW=$FW ($(find "$FW" -maxdepth 1 ( -name ".dylib" -o -name ".so" ) -not -type l 2>/dev/null | wc -l | tr -d ' ') loose, $(find "$FW" -name "*.framework" -type d 2>/dev/null | wc -l | tr -d ' ') frameworks)"

    ============================================================

    Step 0b: 统一 MinimumOSVersion

    ============================================================

    TARGET_MIN_OS="${IPHONEOS_DEPLOYMENT_TARGET:-17.0}"
    echo "wrap-loose-dylibs: Step 0b — harmonizing MinimumOSVersion to $TARGET_MIN_OS"
    _PLIST_FIXED=0
    while IFS= read -r -d '' _plist; do
    /usr/libexec/PlistBuddy -c "Set :MinimumOSVersion $TARGET_MIN_OS" "$_plist" 2>/dev/null
    || /usr/libexec/PlistBuddy -c "Add :MinimumOSVersion string $TARGET_MIN_OS" "$_plist" 2>/dev/null
    _PLIST_FIXED=$((_PLIST_FIXED + 1))
    done < <(find "$FW" -name "Info.plist" -path "*.framework/Info.plist" -print0 2>/dev/null)
    echo " fixed MinimumOSVersion in $_PLIST_FIXED framework Info.plists"

    ============================================================

    Step 0b2: 去除 fat framework 中的 armv7 切片

    ============================================================

    echo "wrap-loose-dylibs: Step 0b2 — stripping armv7 from fat framework binaries"
    _ARMV7_STRIPPED=0
    while IFS= read -r -d '' _fw; do
    _plist="$_fw/Info.plist"
    [ -f "$_plist" ] || continue
    _exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$_plist" 2>/dev/null)
    [ -z "$_exe" ] && continue
    _bin="$_fw/$_exe"
    [ -f "$_bin" ] || continue
    file "$_bin" 2>/dev/null | grep -q "universal binary" || continue
    lipo -archs "$_bin" 2>/dev/null | grep -q "armv7" || continue
    if lipo -remove armv7 "$_bin" -output "$_bin.thinned" 2>/dev/null &&
    mv -f "$_bin.thinned" "$_bin"; then
    _ARMV7_STRIPPED=$((_ARMV7_STRIPPED + 1))
    echo " stripped armv7 from $(basename "$_fw")"
    else
    rm -f "$_bin.thinned"
    fi
    done < <(find "$FW" -name "*.framework" -type d -print0 2>/dev/null)
    echo " $_ARMV7_STRIPPED framework(s) thinned to arm64-only"

    ============================================================

    Step 0b3: 修复 .fwork 文件为绝对路径

    ============================================================

    echo "wrap-loose-dylibs: Step 0b3 — rewriting .fwork files to absolute paths"
    _FWORK_FIXED=0
    _FWORK_SKIPPED=0
    while IFS= read -r -d '' _fwork; do
    _content=$(cat "$_fwork" 2>/dev/null | tr -d '\n')
    [ -z "$_content" ] && continue
    case "$_content" in
    @executable_path/|@rpath/|@loader_path/|/)
    _FWORK_SKIPPED=$((_FWORK_SKIPPED + 1))
    continue ;;
    esac
    printf '%s' "@executable_path/$_content" > "$_fwork"
    _FWORK_FIXED=$((_FWORK_FIXED + 1))
    done < <(find "$APP" -name "*.fwork" -type f -print0 2>/dev/null)
    echo " rewrote $_FWORK_FIXED .fwork files (skipped $_FWORK_SKIPPED already-absolute)"

    ============================================================

    Step 0c: 修补 scipy.linalg.cython_lapack(私有 API 规避)

    ============================================================

    CYTHON_LAPACK_BIN="$FW/site-packages.scipy.linalg.cython_lapack.framework/site-packages.scipy.linalg.cython_lapack"
    if [ -f "$CYTHON_LAPACK_BIN" ]; then
    echo "wrap-loose-dylibs: Step 0c — patching scipy cython_lapack for App Store"
    SDK_PATH=$(xcrun --sdk iphoneos --show-sdk-path 2>/dev/null)
    STUBS_C="${TEMP_DIR:-/tmp}/scipy_lapack_stub.c"
    STUBS_DYLIB="$FW/libscipy_lapack_stubs.framework/libscipy_lapack_stubs"
    mkdir -p "$(dirname "$STUBS_DYLIB")"
    cat > "$STUBS_C" <<'STUB_EOF'
    #include <stddef.h>
    void xerbla_arr_io_(const char *srname, const int *info,
    const int *lerr, size_t srname_len) {
    (void)srname; (void)info; (void)lerr; (void)srname_len;
    }
    STUB_EOF
    _MIN_OS="${IPHONEOS_DEPLOYMENT_TARGET:-17.0}"
    if xcrun -sdk iphoneos clang -arch arm64
    "-mios-version-min=$_MIN_OS"
    -isysroot "$SDK_PATH"
    -dynamiclib
    -install_name "@rpath/libscipy_lapack_stubs.framework/libscipy_lapack_stubs"
    -o "$STUBS_DYLIB"
    "$STUBS_C" 2>&1 | sed 's/^/ /'; then
    echo " built libscipy_lapack_stubs.dylib"
    _STUB_PLIST="$FW/libscipy_lapack_stubs.framework/Info.plist"
    cat > "$_STUB_PLIST" <<'STUB_PLIST_EOF'

    CFBundleDevelopmentRegion en CFBundleExecutable libscipy_lapack_stubs CFBundleIdentifier ai.codebench.dylib.libscipy-lapack-stubs CFBundleInfoDictionaryVersion 6.0 CFBundleName libscipy_lapack_stubs CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleVersion 1 STUB_PLIST_EOF printf ' MinimumOSVersion %s\n' \ "${IPHONEOS_DEPLOYMENT_TARGET:-17.0}" >> "$_STUB_PLIST" cat >> "$_STUB_PLIST" <<'STUB_PLIST_EOF2' STUB_PLIST_EOF2 SCRIPT_DIR="${PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}/scripts" if [ -f "$SCRIPT_DIR/patch-cython-lapack.py" ]; then python3 "$SCRIPT_DIR/patch-cython-lapack.py" \ "$CYTHON_LAPACK_BIN" \ "@rpath/libscipy_lapack_stubs.framework/libscipy_lapack_stubs" \ 2>&1 | sed 's/^/ /' else echo " ⚠ patch-cython-lapack.py missing — cython_lapack still has _xerbla_array__" fi codesign --force --sign "$IDENT" --timestamp=none "$STUBS_DYLIB" 2>/dev/null || true codesign --force --sign "$IDENT" --timestamp=none \ "$FW/libscipy_lapack_stubs.framework" 2>/dev/null || true else echo " ⚠ failed to build libscipy_lapack_stubs.dylib — cython_lapack will trip ITMS-90338" fi fi

    ============================================================

    Step 0d: 创建 libscipy_blas_stubs.framework

    ============================================================

    BLAS_STUBS_DYLIB="$FW/libscipy_blas_stubs.framework/libscipy_blas_stubs"
    if [ ! -f "$BLAS_STUBS_DYLIB" ]; then
    echo "wrap-loose-dylibs: Step 0d — creating libscipy_blas_stubs stub"
    SDK_PATH=$(xcrun --sdk iphoneos --show-sdk-path 2>/dev/null)
    _MIN_OS="${IPHONEOS_DEPLOYMENT_TARGET:-17.0}"
    mkdir -p "$(dirname "$BLAS_STUBS_DYLIB")"
    if xcrun -sdk iphoneos clang -arch arm64
    "-mios-version-min=$_MIN_OS"
    -isysroot "$SDK_PATH"
    -dynamiclib
    -undefined dynamic_lookup
    -install_name "@rpath/libscipy_blas_stubs.framework/libscipy_blas_stubs"
    -o "$BLAS_STUBS_DYLIB"
    -x c /dev/null 2>&1 | sed 's/^/ /'; then
    echo " built libscipy_blas_stubs.dylib"
    _STUB_PLIST="$FW/libscipy_blas_stubs.framework/Info.plist"
    cat > "$_STUB_PLIST" <<'STUB_PLIST_EOF'

    CFBundleDevelopmentRegion en CFBundleExecutable libscipy_blas_stubs CFBundleIdentifier ai.codebench.dylib.libscipy-blas-stubs CFBundleInfoDictionaryVersion 6.0 CFBundleName libscipy_blas_stubs CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleVersion 1 STUB_PLIST_EOF printf ' MinimumOSVersion %s\n' \ "${IPHONEOS_DEPLOYMENT_TARGET:-17.0}" >> "$_STUB_PLIST" cat >> "$_STUB_PLIST" <<'STUB_PLIST_EOF2' STUB_PLIST_EOF2 codesign --force --sign "$IDENT" --timestamp=none "$BLAS_STUBS_DYLIB" 2>/dev/null || true codesign --force --sign "$IDENT" --timestamp=none \ "$FW/libscipy_blas_stubs.framework" 2>/dev/null || true else echo " ⚠ failed to build libscipy_blas_stubs.dylib — BLAS-dependent modules may fail" fi fi

    ============================================================

    辅助函数

    ============================================================

    to_fw_name() {
    local base; base=$(basename "$1" .dylib)
    echo "$base" | tr '.' '_'
    }

    write_plist() {
    local name="$1" out="$2"
    local id_safe; id_safe=$(echo "$name" | tr '_' '-')
    cat > "$out" <<PLIST

    CFBundleDevelopmentRegion en CFBundleExecutable ${name} CFBundleIdentifier ai.codebench.dylib.${id_safe} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${name} CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleVersion 1 MinimumOSVersion ${IPHONEOS_DEPLOYMENT_TARGET:-17.0} PLIST }

    sign_bin() {
    codesign --force --sign "$IDENT" --timestamp=none
    --preserve-metadata=identifier,entitlements,flags "$1" 2>/dev/null
    || codesign --force --sign "$IDENT" --timestamp=none "$1" 2>/dev/null
    || true
    }

    ============================================================

    Step 1: 收集所有松散的 .dylib 并建立重命名映射

    ============================================================

    MAP_FILE="${TEMP_DIR:-/tmp}/wrap-loose-dylibs-$$.map"
    : > "$MAP_FILE"

    while IFS= read -r -d '' dylib; do
    base=$(basename "$dylib")
    name=$(to_fw_name "$dylib")
    fw_dir="$FW/${name}.framework"
    new_load="@rpath/${name}.framework/${name}"
    echo "$base|$new_load|$fw_dir|$fw_dir/$name" >> "$MAP_FILE"
    done < <(find "$FW" -maxdepth 1 -name "*.dylib" -not -type l -print0)

    LOOSE_COUNT=$(wc -l < "$MAP_FILE" | tr -d ' ')
    if [ "$LOOSE_COUNT" -eq 0 ]; then
    echo "wrap-loose-dylibs: 0 loose dylibs — nothing to do"
    rm -f "$MAP_FILE"
    exit 0
    fi
    echo "wrap-loose-dylibs: $LOOSE_COUNT loose dylibs to wrap"

    ============================================================

    Step 2: 将每个松散 .dylib 包装为 .framework

    ============================================================

    while IFS='|' read -r base new_load fw_dir new_bin; do
    [ -z "$base" ] && continue
    src="$FW/$base"
    [ -f "$src" ] || continue
    mkdir -p "$fw_dir"
    mv -f "$src" "$new_bin"
    install_name_tool -id "$new_load" "$new_bin" 2>/dev/null || true
    name=$(basename "$fw_dir" .framework)
    write_plist "$name" "$fw_dir/Info.plist"
    done < "$MAP_FILE"

    清理遗留的符号链接

    for sym in "$FW"/*.dylib; do
    [ -L "$sym" ] || continue
    base=$(basename "$sym")
    name=$(to_fw_name "$sym")
    fw_dir="$FW/${name}.framework"
    if [ -d "$fw_dir" ]; then
    rm -f "$sym"
    fi
    done

    ============================================================

    Step 3: 重写所有二进制中对这些 dylib 的引用

    ============================================================

    rewrite_refs_in() {
    local target="$1"
    [ -f "$target" ] || return
    while IFS='|' read -r base new_load fw_dir new_bin; do
    [ -z "$base" ] && continue
    old_ref="@rpath/$base"

    if otool -L "$target" 2>/dev/null | grep -q "^ $old_ref "; then gbf修改后可以编译成功

    if otool -L "$target" 2>/dev/null | grep -q "$old_ref"; then
            install_name_tool -change "$old_ref" "$new_load" "$target" 2>/dev/null || true
        fi
    done < "$MAP_FILE"
    

    }

    echo "wrap-loose-dylibs: rewriting references in all binaries"

    {
    # 主可执行文件
    [ -f "$MAIN_BIN" ] && printf '%s\0' "$MAIN_BIN"

    # 所有 framework 的可执行文件
    find "$FW" -name "*.framework" -type d -print0 | while IFS= read -r -d '' fw; do
        plist="$fw/Info.plist"
        [ -f "$plist" ] || continue
        exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
        [ -z "$exe" ] && continue
        bin="$fw/$exe"
        [ -f "$bin" ] && printf '%s\0' "$bin"
    done
    
    # 所有 .so 和 .dylib(包括 Python.framework 内部的)
    find "$APP" -type f \( -name "*.so" -o -name "*.dylib" \) -print0 2>/dev/null
    

    } | while IFS= read -r -d '' bin; do
    rewrite_refs_in "$bin"
    done

    确保主可执行文件有 @executable_path/Frameworks 这个 rpath(如果没有则添加)

    if [ -f "$MAIN_BIN" ]; then
    if ! otool -l "$MAIN_BIN" 2>/dev/null | grep -A2 LC_RPATH | grep -q "Frameworks"; then
    install_name_tool -add_rpath "@executable_path/Frameworks" "$MAIN_BIN" 2>/dev/null || true
    fi
    fi

    ============================================================

    Step 3a: 删除静态 .a 归档

    ============================================================

    echo "wrap-loose-dylibs: scrubbing static .a archives"
    A_REMOVED=0
    while IFS= read -r -d '' a; do
    rm -f "$a" && A_REMOVED=$((A_REMOVED + 1))
    done < <(find "$FW" "$APP" -name "*.a" -type f -print0 2>/dev/null)
    echo " removed $A_REMOVED static archives"

    ============================================================

    Step 3b: 将非框架目录(latex/katex)移出 Frameworks/

    ============================================================

    for d in latex katex; do
    if [ -d "$FW/$d" ] && [ ! -d "$FW/${d}.framework" ]; then
    dest_root="$APP"
    [ -d "$APP/Resources" ] && dest_root="$APP/Resources"
    echo " relocating $d/ → ${dest_root#$APP/}/"
    rm -rf "$dest_root/$d"
    mv "$FW/$d" "$dest_root/$d"
    fi
    done

    ============================================================

    Step 3c: 处理 app_packages 深处的松散 dylib

    ============================================================

    APP_PKGS=""
    for cand in "$APP/app_packages" "$APP/Resources/app_packages" "$APP/python/app_packages"; do
    [ -d "$cand" ] && APP_PKGS="$cand" && break
    done

    if [ -n "$APP_PKGS" ]; then
    echo "wrap-loose-dylibs: scrubbing app_packages/ at $APP_PKGS"

    # 替换 torch_shm_manager 为文本占位符
    TORCH_SHM="$APP_PKGS/site-packages/torch/bin/torch_shm_manager"
    if [ -f "$TORCH_SHM" ]; then
        _IS_MACHO=0
        if [ -s "$TORCH_SHM" ]; then
            _MAGIC=$(xxd -l 4 -p "$TORCH_SHM" 2>/dev/null)
            case "$_MAGIC" in
                cffaedfe|cefaedfe|cafebabe|bebafeca) _IS_MACHO=1 ;;
            esac
        fi
        if [ "$_IS_MACHO" = "1" ]; then
            mkdir -p "$(dirname "$TORCH_SHM")"
            cat > "$TORCH_SHM" <<'EOF_SHM_PLACEHOLDER'
    

    iOS placeholder for torch_shm_manager

    The real torch_shm_manager is a Mach-O executable used by

    torch.multiprocessing to spawn shared-memory daemons via fork().

    iOS forbids fork() in app bundles AND App Store rejects standalone

    Mach-O executables in app_packages/, so we replace the binary with

    this text placeholder. PyTorch's manager_path() finds the file,

    os.path.exists() returns True, _C._initExtension() succeeds, and

    torch loads with most features intact (tensors, autograd, nn,

    optim, jit, save/load, ExecuTorch, CoreML — all work).

    torch.multiprocessing's fork-based APIs will fail at call time,

    which is the actual behavior on iOS regardless.

    EOF_SHM_PLACEHOLDER
    chmod 644 "$TORCH_SHM"
    echo " torch_shm_manager: Mach-O → text placeholder (App Store-safe)"
    fi
    fi

    DEEP_MAP="${TEMP_DIR:-/tmp}/wrap-deep-$$.map"
    : > "$DEEP_MAP"
    
    while IFS= read -r -d '' dylib; do
        base=$(basename "$dylib")
        name=$(to_fw_name "$dylib")
        existing="$FW/${name}.framework"
        if [ -d "$existing" ]; then
            rm -f "$dylib"
            continue
        fi
        new_load="@rpath/${name}.framework/${name}"
        new_fw="$FW/${name}.framework"
        new_bin="$new_fw/$name"
        mkdir -p "$new_fw"
        mv -f "$dylib" "$new_bin"
        install_name_tool -id "$new_load" "$new_bin" 2>/dev/null || true
        write_plist "$name" "$new_fw/Info.plist"
        rel=$(python3 -c "import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))" \
              "$new_bin" "$(dirname "$dylib")" 2>/dev/null)
        [ -n "$rel" ] && ln -sf "$rel" "$dylib" 2>/dev/null
        echo "$base|$new_load|$new_fw|$new_bin" >> "$DEEP_MAP"
    done < <(find "$APP_PKGS" -mindepth 2 -name "*.dylib" -type f -print0 2>/dev/null)
    
    DEEP_COUNT=$(wc -l < "$DEEP_MAP" | tr -d ' ')
    if [ "$DEEP_COUNT" -gt 0 ]; then
        echo "  wrapped $DEEP_COUNT deep dylibs; rewriting refs in .so files"
        while IFS= read -r -d '' so; do
            while IFS='|' read -r dbase dnew_load _ _; do
                [ -z "$dbase" ] && continue
                for prefix in "@loader_path/../.dylibs" "@loader_path/.dylibs" "@loader_path/lib" "@rpath"; do
                    old_ref="$prefix/$dbase"
                    if otool -L "$so" 2>/dev/null | grep -q "$old_ref"; then
                        install_name_tool -change "$old_ref" "$dnew_load" "$so" 2>/dev/null || true
                    fi
                done
            done < "$DEEP_MAP"
        done < <(find "$APP_PKGS" -name "*.so" -type f -print0 2>/dev/null)
    fi
    rm -f "$DEEP_MAP"
    

    fi

    ============================================================

    Step 3c2: 去除位码 (bitcode)

    ============================================================

    echo "wrap-loose-dylibs: stripping bitcode from binaries"
    BC_STRIPPED=0
    strip_bitcode() {
    local bin="$1"
    [ -f "$bin" ] || return
    file "$bin" 2>/dev/null | grep -q "Mach-O" || return
    if xcrun bitcode_strip -r "$bin" -o "$bin" 2>/dev/null; then
    BC_STRIPPED=$((BC_STRIPPED + 1))
    fi
    }
    while IFS= read -r -d '' fw; do
    plist="$fw/Info.plist"
    [ -f "$plist" ] || continue
    exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
    [ -n "$exe" ] && strip_bitcode "$fw/$exe"
    done < <(find "$FW" -name ".framework" -type d -print0)
    while IFS= read -r -d '' fw; do
    plist="$fw/Info.plist"
    [ -f "$plist" ] || continue
    exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
    [ -n "$exe" ] && strip_bitcode "$fw/$exe"
    done < <(find "$APP" -path "
    .xcframework/.framework" -type d -print0 2>/dev/null)
    if [ -n "$APP_PKGS" ]; then
    while IFS= read -r -d '' bin; do
    strip_bitcode "$bin"
    done < <(find "$APP_PKGS" ( -name "
    .so" -o -name "*.dylib" ) -type f -not -type l -print0 2>/dev/null)
    fi
    [ -f "$MAIN_BIN" ] && strip_bitcode "$MAIN_BIN"
    echo " stripped bitcode from $BC_STRIPPED binaries"

    ============================================================

    Step 3d: 将 MH_BUNDLE 转换为 MH_DYLIB

    ============================================================

    SCRIPT_DIR="${PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}/scripts"
    FIX_PY="$SCRIPT_DIR/fix-macho-type.py"
    if [ -f "$FIX_PY" ]; then
    echo "wrap-loose-dylibs: flipping MH_BUNDLE → MH_DYLIB"
    python3 "$FIX_PY" "$APP" 2>&1 | sed 's/^/ /'
    else
    echo " ⚠ fix-macho-type.py not found at $FIX_PY — MH_BUNDLE will trip validator"
    fi

    ============================================================

    Step 3e: 修复 libscipy_blas_stubs.dylib 引用

    ============================================================

    echo "wrap-loose-dylibs: fixing references to libscipy_blas_stubs.dylib"
    OLD_BLAS_REF="@rpath/libscipy_blas_stubs.dylib"
    NEW_BLAS_REF="@rpath/libscipy_blas_stubs.framework/libscipy_blas_stubs"

    if [ -f "$MAIN_BIN" ]; then
    if otool -L "$MAIN_BIN" 2>/dev/null | grep -q "$OLD_BLAS_REF"; then
    install_name_tool -change "$OLD_BLAS_REF" "$NEW_BLAS_REF" "$MAIN_BIN" 2>/dev/null && echo " updated main executable"
    fi
    fi

    while IFS= read -r -d '' bin; do
    if otool -L "$bin" 2>/dev/null | grep -q "$OLD_BLAS_REF"; then
    install_name_tool -change "$OLD_BLAS_REF" "$NEW_BLAS_REF" "$bin" 2>/dev/null && echo " updated $bin"
    fi
    done < <(
    find "$FW" -name "*.framework" -type d -print0 |
    while IFS= read -r -d '' fw; do
    plist="$fw/Info.plist"
    [ -f "$plist" ] || continue
    exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
    bin="$fw/$exe"
    [ -f "$bin" ] && printf '%s\0' "$bin"
    done
    )

    ============================================================

    Step 4: 重新签名所有被修改过的二进制和框架

    ============================================================

    while IFS='|' read -r base new_load fw_dir new_bin; do
    [ -z "$base" ] && continue
    sign_bin "$new_bin"
    codesign --force --sign "$IDENT" --timestamp=none "$fw_dir" 2>/dev/null || true
    done < "$MAP_FILE"

    while IFS= read -r -d '' fw; do
    plist="$fw/Info.plist"
    [ -f "$plist" ] || continue
    exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
    bin="$fw/$exe"
    [ -f "$bin" ] && sign_bin "$bin"
    codesign --force --sign "$IDENT" --timestamp=none "$fw" 2>/dev/null || true
    done < <(find "$FW" -name "*.framework" -type d -print0)

    if [ -n "$APP_PKGS" ]; then
    find "$APP_PKGS" -name "*.so" -type f -not -type l -print0 2>/dev/null |
    while IFS= read -r -d '' so; do
    sign_bin "$so"
    done
    fi

    [ -f "$MAIN_BIN" ] && sign_bin "$MAIN_BIN"

    rm -f "$MAP_FILE"

    LEFT=$(find "$FW" -maxdepth 1 ( -name ".dylib" -o -name ".so" ) -not -type l 2>/dev/null | wc -l | tr -d ' ')
    WRAPPED=$(find "$FW" -name ".framework" -type d | wc -l | tr -d ' ')
    DEEP_LEFT=0
    [ -n "$APP_PKGS" ] && DEEP_LEFT=$(find "$APP_PKGS" -name "
    .dylib" -type f -not -type l 2>/dev/null | wc -l | tr -d ' ')
    STATIC_LEFT=$(find "$FW" "$APP" -name ".a" -type f 2>/dev/null | wc -l | tr -d ' ')
    echo "wrap-loose-dylibs: done — $WRAPPED frameworks, $LEFT loose top-level, $DEEP_LEFT loose in app_packages, $STATIC_LEFT static archives"
    if [ "$LEFT" -gt 0 ] || [ "$DEEP_LEFT" -gt 0 ] || [ "$STATIC_LEFT" -gt 0 ]; then
    echo " ⚠ remaining items the validator may reject:"
    find "$FW" -maxdepth 1 ( -name "
    .dylib" -o -name ".so" ) -not -type l 2>/dev/null | sed 's|^| |'
    [ -n "$APP_PKGS" ] && find "$APP_PKGS" -name "
    .dylib" -type f -not -type l 2>/dev/null | sed 's|^| |'
    find "$FW" "$APP" -name "*.a" -type f 2>/dev/null | sed 's|^| |'
    fi

    ============================================================

    最终 App Store 就绪检查

    ============================================================

    echo "=== wrap-loose-dylibs: final App Store readiness check ==="
    _REJECTED=0
    _check_absent() {
    local label="$1"; shift
    local found=()
    for _path in "$@"; do
    [ -e "$_path" ] && found+=("$_path")
    done
    if [ ${#found[@]} -gt 0 ]; then
    echo " ❌ $label still present:"
    for _f in "${found[@]}"; do echo " $_f"; done
    _REJECTED=$((_REJECTED + ${#found[@]}))
    else
    echo " ✓ $label clean"
    fi
    }
    _static_archives=()
    while IFS= read -r -d '' _a; do _static_archives+=("$_a"); done < <(find "$APP" -name ".a" -type f -print0 2>/dev/null)
    _check_absent "static .a archives" "${_static_archives[@]}"
    _loose_dylibs=()
    while IFS= read -r -d '' _d; do _loose_dylibs+=("$_d"); done < <(find "$FW" -maxdepth 1 -name "
    .dylib" -type f -print0 2>/dev/null)
    _check_absent "loose .dylib in Frameworks/" "${_loose_dylibs[@]}"
    if [ -n "$APP_PKGS" ]; then
    _deep_dylibs=()
    while IFS= read -r -d '' _d; do _deep_dylibs+=("$_d"); done < <(find "$APP_PKGS" -name ".dylib" -type f -not -type l -print0 2>/dev/null)
    _check_absent "loose .dylib in app_packages/" "${_deep_dylibs[@]}"
    fi
    _TORCH_SHM_PATH="$APP/app_packages/site-packages/torch/bin/torch_shm_manager"
    if [ -f "$_TORCH_SHM_PATH" ]; then
    if file "$_TORCH_SHM_PATH" 2>/dev/null | grep -q "Mach-O"; then
    echo " ❌ torch_shm_manager is still a Mach-O — App Store will reject"
    _REJECTED=$((_REJECTED + 1))
    else
    echo " ✓ torch_shm_manager is text placeholder (App Store-safe)"
    fi
    else
    echo " ⚠ torch_shm_manager missing — torch.multiprocessing won't initialize"
    fi
    _check_absent "manimpango native .so (CTFontCopyDefaultCascadeList)"
    "$APP/app_packages/site-packages/manimpango/_register_font.cpython-314-iphoneos.so"
    "$APP/app_packages/site-packages/manimpango/cmanimpango.cpython-314-iphoneos.so"
    _check_absent "psutil._psutil_osx (IOPSCopyPowerSources
    )"
    "$APP/app_packages/site-packages/psutil/_psutil_osx.abi3.so"
    [ -d "$FW/latex" ] && [ ! -d "$FW/latex.framework" ] &&
    echo " ❌ Frameworks/latex/ data dir still present" && _REJECTED=$((_REJECTED + 1))
    [ -d "$FW/katex" ] && [ ! -d "$FW/katex.framework" ] &&
    echo " ❌ Frameworks/katex/ data dir still present" && _REJECTED=$((_REJECTED + 1))

    if [ "$_REJECTED" -gt 0 ]; then
    echo ""
    echo "❌ App Store readiness FAILED: $_REJECTED rejected file(s) still in bundle."
    echo " App Store Connect will reject this upload. Fix above issues before archiving."
    exit 1
    fi
    echo "✓ App Store readiness OK"
    exit 0

  7. baixiaochunbudanchun commented on Jun 2, 2026

    @baixiaochunbudanchun
    Author

    第二进行打包提交商店审核,
    下面是提交包的项目文件结构,

    Image 提交过程报了如下一系列错误 Invalid bundle structure. The “CustomCalculator.app/python-stdlib/lib-dynload/_interpreters.cpython-314-iphoneos.so” binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle The server’s response was: ‘{ code = 90171; description = "Invalid bundle structure. The \U201cCustomCalculator.app/python-stdlib/lib-dynload/_interpreters.cpython-314-iphoneos.so\U201d binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle"; }’.

    Invalid bundle structure. The “CustomCalculator.app/python-ios-lib_SciPy.bundle/scipy/linalg/_fblas.cpython-314-iphoneos.so” binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
    The server’s response was: ‘{
    code = 90171;
    description = "Invalid bundle structure. The \U201cCustomCalculator.app/python-ios-lib_SciPy.bundle/scipy/linalg/_fblas.cpython-314-iphoneos.so\U201d binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle";
    }’.

    Invalid bundle structure. The “CustomCalculator.app/python-ios-lib_NumPy.bundle/numpy/random/_mt19937.cpython-314-iphoneos.so” binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
    The server’s response was: ‘{
    code = 90171;
    description = "Invalid bundle structure. The \U201cCustomCalculator.app/python-ios-lib_NumPy.bundle/numpy/random/_mt19937.cpython-314-iphoneos.so\U201d binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle";
    }’.

    Upload Symbols Failed
    The archive did not include a dSYM for the Python.framework with the UUIDs [89F1E796-CAC3-3DCF-AE52-9F1D7BB1AEA5]. Ensure that the archive's dSYM folder includes a DWARF file for Python.framework with the expected UUIDs.

  8. baixiaochunbudanchun commented on Jun 2, 2026

    @baixiaochunbudanchun
    Author

    第三步我继续需修改wrap-loose-dylibs 脚本
    #!/bin/bash

    wrap-loose-dylibs.sh — App Store fix for CodeBench / hLifeKup

    ===============================================================

    1. 将 Frameworks/ 中的松散 .dylib 包装为 .framework

    2. 将 .bundle 和 lib-dynload 中的 .so 包装为 .framework

    3. 重写所有二进制中对这些 dylib/so 的引用(包括主可执行文件、已有 framework 等)

    4. 清理 App Store 会拒绝的静态库、位码、私有 API 等

    5. 重新签名所有修改过的二进制

    Build Settings 要求:ENABLE_USER_SCRIPT_SANDBOXING = NO

    ===============================================================

    set +e

    echo "=== wrap-loose-dylibs.sh starting ==="
    echo " BUILT_PRODUCTS_DIR=${BUILT_PRODUCTS_DIR:-(unset)}"
    echo " WRAPPER_NAME=${WRAPPER_NAME:-(unset)}"
    echo " EXECUTABLE_NAME=${EXECUTABLE_NAME:-(unset)}"
    echo " EXPANDED_CODE_SIGN_IDENTITY=${EXPANDED_CODE_SIGN_IDENTITY:-(unset)}"
    echo " ENABLE_USER_SCRIPT_SANDBOXING=${ENABLE_USER_SCRIPT_SANDBOXING:-(unset)}"

    确定 .app 路径(优先使用 CODESIGNING_FOLDER_PATH)

    if [ -n "${CODESIGNING_FOLDER_PATH}" ] && [ -d "${CODESIGNING_FOLDER_PATH}" ]; then
    APP="${CODESIGNING_FOLDER_PATH}"
    else
    APP="${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}"
    fi
    FW="$APP/Frameworks"
    IDENT="${EXPANDED_CODE_SIGN_IDENTITY:--}"

    if [ ! -d "$APP" ]; then
    for _alt in
    "${TARGET_BUILD_DIR}/${WRAPPER_NAME}"
    "${INSTALL_DIR}/${WRAPPER_NAME}"
    "${INSTALL_ROOT}${INSTALL_PATH}/${WRAPPER_NAME}"
    "${DSTROOT}${INSTALL_PATH}/${WRAPPER_NAME}"; do
    if [ -d "$_alt" ]; then
    APP="$_alt"
    FW="$APP/Frameworks"
    echo "wrap-loose-dylibs: using fallback APP=$APP"
    break
    fi
    done
    fi
    if [ ! -d "$APP" ]; then
    echo "wrap-loose-dylibs: ERROR APP dir missing — refusing to silently skip"
    echo " BUILT_PRODUCTS_DIR=${BUILT_PRODUCTS_DIR}"
    echo " TARGET_BUILD_DIR=${TARGET_BUILD_DIR}"
    echo " INSTALL_DIR=${INSTALL_DIR}"
    echo " DSTROOT=${DSTROOT}"
    exit 1
    fi
    if [ ! -d "$FW" ]; then
    echo "wrap-loose-dylibs: no Frameworks/ dir at $FW — Install Python phase didn't run yet"
    exit 1
    fi

    ========== 提前定义主可执行文件路径 ==========

    MAIN_BIN="$APP/$EXECUTABLE_NAME"

    ============================================

    ============================================================

    Step 0: 硬删除 App Store 明确拒绝的文件

    ============================================================

    echo "wrap-loose-dylibs: Step 0 — hard cleanup of App Store-rejected files"
    _HARD_CLEANUP_PATHS=(
    "$FW/libkernels_optimized_ios.a"
    "$FW/libexecutorch_ios.a"
    "$FW/libbackend_coreml_ios.a"
    "$FW/libbackend_xnnpack_ios.a"
    "$FW/libthreadpool_ios.a"
    "$APP/app_packages/site-packages/numpy/_core/lib/libnpymath.a"
    "$APP/app_packages/site-packages/numpy/random/lib/libnpyrandom.a"
    "$APP/app_packages/site-packages/scipy/.dylibs/libgcc_s.1.1.dylib"
    "$APP/app_packages/site-packages/scipy/.dylibs/libgfortran.5.dylib"
    "$APP/app_packages/site-packages/scipy/.dylibs/libquadmath.0.dylib"
    "$FW/libgcc_s_1_1.framework"
    "$FW/libgfortran_5.framework"
    "$FW/libquadmath_0.framework"
    "$APP/app_packages/site-packages/manimpango/_register_font.cpython-314-iphoneos.so"
    "$APP/app_packages/site-packages/manimpango/cmanimpango.cpython-314-iphoneos.so"
    "$APP/app_packages/site-packages/manimpango/enums.cpython-314-iphoneos.so"
    "$FW/site-packages.manimpango._register_font.framework"
    "$FW/site-packages.manimpango.cmanimpango.framework"
    "$FW/site-packages.manimpango.enums.framework"
    "$APP/app_packages/site-packages/psutil/_psutil_osx.abi3.so"
    "$APP/app_packages/site-packages/psutil/_psutil_osx.abi3.fwork"
    "$FW/site-packages.psutil._psutil_osx.framework"
    )
    for _f in "${_HARD_CLEANUP_PATHS[@]}"; do
    if [ -e "$_f" ]; then
    rm -rf "$_f" && echo " removed $_f"
    fi
    done
    echo " APP=$APP ($(du -sh "$APP" 2>/dev/null | awk '{print $1}'))"
    echo " FW=$FW ($(find "$FW" -maxdepth 1 ( -name ".dylib" -o -name ".so" ) -not -type l 2>/dev/null | wc -l | tr -d ' ') loose, $(find "$FW" -name "*.framework" -type d 2>/dev/null | wc -l | tr -d ' ') frameworks)"

    ============================================================

    Step 0b: 统一 MinimumOSVersion

    ============================================================

    TARGET_MIN_OS="${IPHONEOS_DEPLOYMENT_TARGET:-17.0}"
    echo "wrap-loose-dylibs: Step 0b — harmonizing MinimumOSVersion to $TARGET_MIN_OS"
    _PLIST_FIXED=0
    while IFS= read -r -d '' _plist; do
    /usr/libexec/PlistBuddy -c "Set :MinimumOSVersion $TARGET_MIN_OS" "$_plist" 2>/dev/null
    || /usr/libexec/PlistBuddy -c "Add :MinimumOSVersion string $TARGET_MIN_OS" "$_plist" 2>/dev/null
    _PLIST_FIXED=$((_PLIST_FIXED + 1))
    done < <(find "$FW" -name "Info.plist" -path "*.framework/Info.plist" -print0 2>/dev/null)
    echo " fixed MinimumOSVersion in $_PLIST_FIXED framework Info.plists"

    ============================================================

    Step 0b2: 去除 fat framework 中的 armv7 切片

    ============================================================

    echo "wrap-loose-dylibs: Step 0b2 — stripping armv7 from fat framework binaries"
    _ARMV7_STRIPPED=0
    while IFS= read -r -d '' _fw; do
    _plist="$_fw/Info.plist"
    [ -f "$_plist" ] || continue
    _exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$_plist" 2>/dev/null)
    [ -z "$_exe" ] && continue
    _bin="$_fw/$_exe"
    [ -f "$_bin" ] || continue
    file "$_bin" 2>/dev/null | grep -q "universal binary" || continue
    lipo -archs "$_bin" 2>/dev/null | grep -q "armv7" || continue
    if lipo -remove armv7 "$_bin" -output "$_bin.thinned" 2>/dev/null &&
    mv -f "$_bin.thinned" "$_bin"; then
    _ARMV7_STRIPPED=$((_ARMV7_STRIPPED + 1))
    echo " stripped armv7 from $(basename "$_fw")"
    else
    rm -f "$_bin.thinned"
    fi
    done < <(find "$FW" -name "*.framework" -type d -print0 2>/dev/null)
    echo " $_ARMV7_STRIPPED framework(s) thinned to arm64-only"

    ============================================================

    Step 0b3: 修复 .fwork 文件为绝对路径

    ============================================================

    echo "wrap-loose-dylibs: Step 0b3 — rewriting .fwork files to absolute paths"
    _FWORK_FIXED=0
    _FWORK_SKIPPED=0
    while IFS= read -r -d '' _fwork; do
    _content=$(cat "$_fwork" 2>/dev/null | tr -d '\n')
    [ -z "$_content" ] && continue
    case "$_content" in
    @executable_path/|@rpath/|@loader_path/|/)
    _FWORK_SKIPPED=$((_FWORK_SKIPPED + 1))
    continue ;;
    esac
    printf '%s' "@executable_path/$_content" > "$_fwork"
    _FWORK_FIXED=$((_FWORK_FIXED + 1))
    done < <(find "$APP" -name "*.fwork" -type f -print0 2>/dev/null)
    echo " rewrote $_FWORK_FIXED .fwork files (skipped $_FWORK_SKIPPED already-absolute)"

    ============================================================

    Step 0c: 修补 scipy.linalg.cython_lapack(私有 API 规避)

    ============================================================

    CYTHON_LAPACK_BIN="$FW/site-packages.scipy.linalg.cython_lapack.framework/site-packages.scipy.linalg.cython_lapack"
    if [ -f "$CYTHON_LAPACK_BIN" ]; then
    echo "wrap-loose-dylibs: Step 0c — patching scipy cython_lapack for App Store"
    SDK_PATH=$(xcrun --sdk iphoneos --show-sdk-path 2>/dev/null)
    STUBS_C="${TEMP_DIR:-/tmp}/scipy_lapack_stub.c"
    STUBS_DYLIB="$FW/libscipy_lapack_stubs.framework/libscipy_lapack_stubs"
    mkdir -p "$(dirname "$STUBS_DYLIB")"
    cat > "$STUBS_C" <<'STUB_EOF'
    #include <stddef.h>
    void xerbla_arr_io_(const char *srname, const int *info,
    const int *lerr, size_t srname_len) {
    (void)srname; (void)info; (void)lerr; (void)srname_len;
    }
    STUB_EOF
    _MIN_OS="${IPHONEOS_DEPLOYMENT_TARGET:-17.0}"
    if xcrun -sdk iphoneos clang -arch arm64
    "-mios-version-min=$_MIN_OS"
    -isysroot "$SDK_PATH"
    -dynamiclib
    -install_name "@rpath/libscipy_lapack_stubs.framework/libscipy_lapack_stubs"
    -o "$STUBS_DYLIB"
    "$STUBS_C" 2>&1 | sed 's/^/ /'; then
    echo " built libscipy_lapack_stubs.dylib"
    _STUB_PLIST="$FW/libscipy_lapack_stubs.framework/Info.plist"
    cat > "$_STUB_PLIST" <<'STUB_PLIST_EOF'

    CFBundleDevelopmentRegion en CFBundleExecutable libscipy_lapack_stubs CFBundleIdentifier ai.codebench.dylib.libscipy-lapack-stubs CFBundleInfoDictionaryVersion 6.0 CFBundleName libscipy_lapack_stubs CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleVersion 1 STUB_PLIST_EOF printf ' MinimumOSVersion %s\n' \ "${IPHONEOS_DEPLOYMENT_TARGET:-17.0}" >> "$_STUB_PLIST" cat >> "$_STUB_PLIST" <<'STUB_PLIST_EOF2' STUB_PLIST_EOF2 SCRIPT_DIR="${PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}/scripts" if [ -f "$SCRIPT_DIR/patch-cython-lapack.py" ]; then python3 "$SCRIPT_DIR/patch-cython-lapack.py" \ "$CYTHON_LAPACK_BIN" \ "@rpath/libscipy_lapack_stubs.framework/libscipy_lapack_stubs" \ 2>&1 | sed 's/^/ /' else echo " ⚠ patch-cython-lapack.py missing — cython_lapack still has _xerbla_array__" fi codesign --force --sign "$IDENT" --timestamp=none "$STUBS_DYLIB" 2>/dev/null || true codesign --force --sign "$IDENT" --timestamp=none \ "$FW/libscipy_lapack_stubs.framework" 2>/dev/null || true else echo " ⚠ failed to build libscipy_lapack_stubs.dylib — cython_lapack will trip ITMS-90338" fi fi

    ============================================================

    Step 0d: 创建 libscipy_blas_stubs.framework

    ============================================================

    BLAS_STUBS_DYLIB="$FW/libscipy_blas_stubs.framework/libscipy_blas_stubs"
    if [ ! -f "$BLAS_STUBS_DYLIB" ]; then
    echo "wrap-loose-dylibs: Step 0d — creating libscipy_blas_stubs stub"
    SDK_PATH=$(xcrun --sdk iphoneos --show-sdk-path 2>/dev/null)
    _MIN_OS="${IPHONEOS_DEPLOYMENT_TARGET:-17.0}"
    mkdir -p "$(dirname "$BLAS_STUBS_DYLIB")"
    if xcrun -sdk iphoneos clang -arch arm64
    "-mios-version-min=$_MIN_OS"
    -isysroot "$SDK_PATH"
    -dynamiclib
    -undefined dynamic_lookup
    -install_name "@rpath/libscipy_blas_stubs.framework/libscipy_blas_stubs"
    -o "$BLAS_STUBS_DYLIB"
    -x c /dev/null 2>&1 | sed 's/^/ /'; then
    echo " built libscipy_blas_stubs.dylib"
    _STUB_PLIST="$FW/libscipy_blas_stubs.framework/Info.plist"
    cat > "$_STUB_PLIST" <<'STUB_PLIST_EOF'

    CFBundleDevelopmentRegion en CFBundleExecutable libscipy_blas_stubs CFBundleIdentifier ai.codebench.dylib.libscipy-blas-stubs CFBundleInfoDictionaryVersion 6.0 CFBundleName libscipy_blas_stubs CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleVersion 1 STUB_PLIST_EOF printf ' MinimumOSVersion %s\n' \ "${IPHONEOS_DEPLOYMENT_TARGET:-17.0}" >> "$_STUB_PLIST" cat >> "$_STUB_PLIST" <<'STUB_PLIST_EOF2' STUB_PLIST_EOF2 codesign --force --sign "$IDENT" --timestamp=none "$BLAS_STUBS_DYLIB" 2>/dev/null || true codesign --force --sign "$IDENT" --timestamp=none \ "$FW/libscipy_blas_stubs.framework" 2>/dev/null || true else echo " ⚠ failed to build libscipy_blas_stubs.dylib — BLAS-dependent modules may fail" fi fi

    ============================================================

    辅助函数

    ============================================================

    to_fw_name() {
    local file_path="$1"
    local base
    # 如果路径中有 .bundle,则提取 .bundle 名称
    if [[ "$file_path" == ".bundle" ]]; then
    # 提取 .bundle 前缀
    base=$(basename "$(dirname "$(dirname "$file_path")")" .bundle)
    else
    base=$(basename "$file_path" .so)
    base=${base%.dylib}
    fi
    # 将名称中的点号替换为下划线
    echo "$base" | tr '.' '_'
    }

    write_plist() {
    local name="$1" out="$2"
    local id_safe; id_safe=$(echo "$name" | tr '_' '-')
    cat > "$out" <<PLIST

    CFBundleDevelopmentRegion en CFBundleExecutable ${name} CFBundleIdentifier ai.codebench.dylib.${id_safe} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${name} CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleVersion 1 MinimumOSVersion ${IPHONEOS_DEPLOYMENT_TARGET:-17.0} PLIST }

    sign_bin() {
    codesign --force --sign "$IDENT" --timestamp=none
    --preserve-metadata=identifier,entitlements,flags "$1" 2>/dev/null
    || codesign --force --sign "$IDENT" --timestamp=none "$1" 2>/dev/null
    || true
    }

    ============================================================

    Step 1: 收集所有松散的 .dylib 并建立重命名映射

    ============================================================

    MAP_FILE="${TEMP_DIR:-/tmp}/wrap-loose-dylibs-$$.map"
    : > "$MAP_FILE"

    while IFS= read -r -d '' dylib; do
    base=$(basename "$dylib")
    name=$(to_fw_name "$dylib")
    fw_dir="$FW/${name}.framework"
    new_load="@rpath/${name}.framework/${name}"
    echo "$base|$new_load|$fw_dir|$fw_dir/$name" >> "$MAP_FILE"
    done < <(find "$FW" -maxdepth 1 -name "*.dylib" -not -type l -print0)

    LOOSE_COUNT=$(wc -l < "$MAP_FILE" | tr -d ' ')
    if [ "$LOOSE_COUNT" -eq 0 ]; then
    echo "wrap-loose-dylibs: 0 loose dylibs — nothing to do"
    rm -f "$MAP_FILE"
    # 继续执行后续步骤,可能还有 .so 需要处理
    else
    echo "wrap-loose-dylibs: $LOOSE_COUNT loose dylibs to wrap"

    # ============================================================
    # Step 2: 将每个松散 .dylib 包装为 .framework
    # ============================================================
    while IFS='|' read -r base new_load fw_dir new_bin; do
        [ -z "$base" ] && continue
        src="$FW/$base"
        [ -f "$src" ] || continue
        mkdir -p "$fw_dir"
        mv -f "$src" "$new_bin"
        install_name_tool -id "$new_load" "$new_bin" 2>/dev/null || true
        name=$(basename "$fw_dir" .framework)
        write_plist "$name" "$fw_dir/Info.plist"
    done < "$MAP_FILE"
    
    # 清理遗留的符号链接
    for sym in "$FW"/*.dylib; do
        [ -L "$sym" ] || continue
        base=$(basename "$sym")
        name=$(to_fw_name "$sym")
        fw_dir="$FW/${name}.framework"
        if [ -d "$fw_dir" ]; then
            rm -f "$sym"
        fi
    done
    

    fi

    ============================================================

    Step 3: 重写所有二进制中对这些 dylib 的引用 gbf修改后可以编译成功

    ============================================================

    rewrite_refs_in() {
    local target="$1"
    [ -f "$target" ] || return
    while IFS='|' read -r base new_load fw_dir new_bin; do
    [ -z "$base" ] && continue
    old_ref="@rpath/$base"
    if otool -L "$target" 2>/dev/null | grep -q "$old_ref"; then
    install_name_tool -change "$old_ref" "$new_load" "$target" 2>/dev/null || true
    fi
    done < "$MAP_FILE"
    }

    echo "wrap-loose-dylibs: rewriting references in all binaries"

    {
    # 主可执行文件
    [ -f "$MAIN_BIN" ] && printf '%s\0' "$MAIN_BIN"

    # 所有 framework 的可执行文件
    find "$FW" -name "*.framework" -type d -print0 | while IFS= read -r -d '' fw; do
        plist="$fw/Info.plist"
        [ -f "$plist" ] || continue
        exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
        [ -z "$exe" ] && continue
        bin="$fw/$exe"
        [ -f "$bin" ] && printf '%s\0' "$bin"
    done
    
    # 所有 .so 和 .dylib(包括 Python.framework 内部的)
    find "$APP" -type f \( -name "*.so" -o -name "*.dylib" \) -print0 2>/dev/null
    

    } | while IFS= read -r -d '' bin; do
    rewrite_refs_in "$bin"
    done

    确保主可执行文件有 @executable_path/Frameworks 这个 rpath(如果没有则添加)

    if [ -f "$MAIN_BIN" ]; then
    if ! otool -l "$MAIN_BIN" 2>/dev/null | grep -A2 LC_RPATH | grep -q "Frameworks"; then
    install_name_tool -add_rpath "@executable_path/Frameworks" "$MAIN_BIN" 2>/dev/null || true
    fi
    fi

    ============================================================

    Step 3a: 删除静态 .a 归档

    ============================================================

    echo "wrap-loose-dylibs: scrubbing static .a archives"
    A_REMOVED=0
    while IFS= read -r -d '' a; do
    rm -f "$a" && A_REMOVED=$((A_REMOVED + 1))
    done < <(find "$FW" "$APP" -name "*.a" -type f -print0 2>/dev/null)
    echo " removed $A_REMOVED static archives"

    ============================================================

    Step 3b: 将非框架目录(latex/katex)移出 Frameworks/

    ============================================================

    for d in latex katex; do
    if [ -d "$FW/$d" ] && [ ! -d "$FW/${d}.framework" ]; then
    dest_root="$APP"
    [ -d "$APP/Resources" ] && dest_root="$APP/Resources"
    echo " relocating $d/ → ${dest_root#$APP/}/"
    rm -rf "$dest_root/$d"
    mv "$FW/$d" "$dest_root/$d"
    fi
    done

    ============================================================

    Step 3c: 处理 app_packages 深处的松散 dylib

    ============================================================

    APP_PKGS=""
    for cand in "$APP/app_packages" "$APP/Resources/app_packages" "$APP/python/app_packages"; do
    [ -d "$cand" ] && APP_PKGS="$cand" && break
    done

    if [ -n "$APP_PKGS" ]; then
    echo "wrap-loose-dylibs: scrubbing app_packages/ at $APP_PKGS"

    # 替换 torch_shm_manager 为文本占位符
    TORCH_SHM="$APP_PKGS/site-packages/torch/bin/torch_shm_manager"
    if [ -f "$TORCH_SHM" ]; then
        _IS_MACHO=0
        if [ -s "$TORCH_SHM" ]; then
            _MAGIC=$(xxd -l 4 -p "$TORCH_SHM" 2>/dev/null)
            case "$_MAGIC" in
                cffaedfe|cefaedfe|cafebabe|bebafeca) _IS_MACHO=1 ;;
            esac
        fi
        if [ "$_IS_MACHO" = "1" ]; then
            mkdir -p "$(dirname "$TORCH_SHM")"
            cat > "$TORCH_SHM" <<'EOF_SHM_PLACEHOLDER'
    

    iOS placeholder for torch_shm_manager

    The real torch_shm_manager is a Mach-O executable used by

    torch.multiprocessing to spawn shared-memory daemons via fork().

    iOS forbids fork() in app bundles AND App Store rejects standalone

    Mach-O executables in app_packages/, so we replace the binary with

    this text placeholder. PyTorch's manager_path() finds the file,

    os.path.exists() returns True, _C._initExtension() succeeds, and

    torch loads with most features intact (tensors, autograd, nn,

    optim, jit, save/load, ExecuTorch, CoreML — all work).

    torch.multiprocessing's fork-based APIs will fail at call time,

    which is the actual behavior on iOS regardless.

    EOF_SHM_PLACEHOLDER
    chmod 644 "$TORCH_SHM"
    echo " torch_shm_manager: Mach-O → text placeholder (App Store-safe)"
    fi
    fi

    DEEP_MAP="${TEMP_DIR:-/tmp}/wrap-deep-$$.map"
    : > "$DEEP_MAP"
    
    while IFS= read -r -d '' dylib; do
        base=$(basename "$dylib")
        name=$(to_fw_name "$dylib")
        existing="$FW/${name}.framework"
        if [ -d "$existing" ]; then
            rm -f "$dylib"
            continue
        fi
        new_load="@rpath/${name}.framework/${name}"
        new_fw="$FW/${name}.framework"
        new_bin="$new_fw/$name"
        mkdir -p "$new_fw"
        mv -f "$dylib" "$new_bin"
        install_name_tool -id "$new_load" "$new_bin" 2>/dev/null || true
        write_plist "$name" "$new_fw/Info.plist"
        rel=$(python3 -c "import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))" \
              "$new_bin" "$(dirname "$dylib")" 2>/dev/null)
        [ -n "$rel" ] && ln -sf "$rel" "$dylib" 2>/dev/null
        echo "$base|$new_load|$new_fw|$new_bin" >> "$DEEP_MAP"
    done < <(find "$APP_PKGS" -mindepth 2 -name "*.dylib" -type f -print0 2>/dev/null)
    
    DEEP_COUNT=$(wc -l < "$DEEP_MAP" | tr -d ' ')
    if [ "$DEEP_COUNT" -gt 0 ]; then
        echo "  wrapped $DEEP_COUNT deep dylibs; rewriting refs in .so files"
        while IFS= read -r -d '' so; do
            while IFS='|' read -r dbase dnew_load _ _; do
                [ -z "$dbase" ] && continue
                for prefix in "@loader_path/../.dylibs" "@loader_path/.dylibs" "@loader_path/lib" "@rpath"; do
                    old_ref="$prefix/$dbase"
                    if otool -L "$so" 2>/dev/null | grep -q "$old_ref"; then
                        install_name_tool -change "$old_ref" "$dnew_load" "$so" 2>/dev/null || true
                    fi
                done
            done < "$DEEP_MAP"
        done < <(find "$APP_PKGS" -name "*.so" -type f -print0 2>/dev/null)
    fi
    rm -f "$DEEP_MAP"
    

    fi

    ============================================================

    Step 3c2: 去除位码 (bitcode)

    ============================================================

    echo "wrap-loose-dylibs: stripping bitcode from binaries"
    BC_STRIPPED=0
    strip_bitcode() {
    local bin="$1"
    [ -f "$bin" ] || return
    file "$bin" 2>/dev/null | grep -q "Mach-O" || return
    if xcrun bitcode_strip -r "$bin" -o "$bin" 2>/dev/null; then
    BC_STRIPPED=$((BC_STRIPPED + 1))
    fi
    }
    while IFS= read -r -d '' fw; do
    plist="$fw/Info.plist"
    [ -f "$plist" ] || continue
    exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
    [ -n "$exe" ] && strip_bitcode "$fw/$exe"
    done < <(find "$FW" -name ".framework" -type d -print0)
    while IFS= read -r -d '' fw; do
    plist="$fw/Info.plist"
    [ -f "$plist" ] || continue
    exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
    [ -n "$exe" ] && strip_bitcode "$fw/$exe"
    done < <(find "$APP" -path "
    .xcframework/.framework" -type d -print0 2>/dev/null)
    if [ -n "$APP_PKGS" ]; then
    while IFS= read -r -d '' bin; do
    strip_bitcode "$bin"
    done < <(find "$APP_PKGS" ( -name "
    .so" -o -name "*.dylib" ) -type f -not -type l -print0 2>/dev/null)
    fi
    [ -f "$MAIN_BIN" ] && strip_bitcode "$MAIN_BIN"
    echo " stripped bitcode from $BC_STRIPPED binaries"

    ============================================================

    Step 3d: 将 MH_BUNDLE 转换为 MH_DYLIB

    ============================================================

    SCRIPT_DIR="${PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}/scripts"
    FIX_PY="$SCRIPT_DIR/fix-macho-type.py"
    if [ -f "$FIX_PY" ]; then
    echo "wrap-loose-dylibs: flipping MH_BUNDLE → MH_DYLIB"
    python3 "$FIX_PY" "$APP" 2>&1 | sed 's/^/ /'
    else
    echo " ⚠ fix-macho-type.py not found at $FIX_PY — MH_BUNDLE will trip validator"
    fi

    ============================================================

    Step 3e: 修复 libscipy_blas_stubs.dylib 引用

    ============================================================

    echo "wrap-loose-dylibs: fixing references to libscipy_blas_stubs.dylib"
    OLD_BLAS_REF="@rpath/libscipy_blas_stubs.dylib"
    NEW_BLAS_REF="@rpath/libscipy_blas_stubs.framework/libscipy_blas_stubs"

    if [ -f "$MAIN_BIN" ]; then
    if otool -L "$MAIN_BIN" 2>/dev/null | grep -q "$OLD_BLAS_REF"; then
    install_name_tool -change "$OLD_BLAS_REF" "$NEW_BLAS_REF" "$MAIN_BIN" 2>/dev/null && echo " updated main executable"
    fi
    fi

    while IFS= read -r -d '' bin; do
    if otool -L "$bin" 2>/dev/null | grep -q "$OLD_BLAS_REF"; then
    install_name_tool -change "$OLD_BLAS_REF" "$NEW_BLAS_REF" "$bin" 2>/dev/null && echo " updated $bin"
    fi
    done < <(
    find "$FW" -name "*.framework" -type d -print0 |
    while IFS= read -r -d '' fw; do
    plist="$fw/Info.plist"
    [ -f "$plist" ] || continue
    exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
    bin="$fw/$exe"
    [ -f "$bin" ] && printf '%s\0' "$bin"
    done
    )

    ============================================================

    新增: 处理 .bundle 和 lib-dynload 中的 .so 文件

    ============================================================

    wrap_so_files() {
    echo "wrap-loose-dylibs: wrapping .so files to .framework..."

    # 定义需要扫描的目录
    local BUNDLE_DIRS=("$APP"/python-ios-lib_*.bundle)
    local DYNLOAD_DIR="$APP/python-stdlib/lib-dynload"
    local BIN_DIRS=("${BUNDLE_DIRS[@]}" "$DYNLOAD_DIR")
    
    # 存储需要处理的 .so 文件列表
    local SO_FILES=()
    
    # 遍历所有目录收集 .so 文件
    for dir in "${BIN_DIRS[@]}"; do
        [ -d "$dir" ] || continue
        while IFS= read -r -d '' so_file; do
            SO_FILES+=("$so_file")
        done < <(find "$dir" -type f -name "*.so" -print0 2>/dev/null)
    done
    
    # 如果没有 .so 文件需要处理,直接返回
    if [ ${#SO_FILES[@]} -eq 0 ]; then
        echo "  no .so files found in bundles or lib-dynload"
        return 0
    fi
    echo "  found ${#SO_FILES[@]} .so files to wrap"
    
    # 为每个 .so 文件创建 framework
    local SO_MAP="${TEMP_DIR:-/tmp}/wrap-so-$$.map"
    : > "$SO_MAP"
    
    local wrapped_count=0
    for so_file in "${SO_FILES[@]}"; do
        # 生成框架名称和路径
        local fw_name=$(to_fw_name "$so_file")
        local fw_dir="$FW/${fw_name}.framework"
        local fw_bin="$fw_dir/${fw_name}"
        local new_load="@rpath/${fw_name}.framework/${fw_name}"
        
        # 跳过已存在的框架
        if [ -d "$fw_dir" ]; then
            echo "    skipping $so_file (framework already exists)"
            continue
        fi
        
        echo "    wrapping $so_file -> $fw_name.framework"
        
        # 创建 framework 结构
        mkdir -p "$fw_dir"
        
        # 移动并重命名 .so 文件
        cp "$so_file" "$fw_bin"
        
        # 修改动态库的 ID
        install_name_tool -id "$new_load" "$fw_bin" 2>/dev/null || true
        
        # 创建 Info.plist
        write_plist "$fw_name" "$fw_dir/Info.plist"
        
        # 记录映射关系用于后续重写引用
        local old_name=$(basename "$so_file")
        local new_fw_path="$fw_dir/$fw_name"
        echo "$old_name|$new_load|$fw_dir|$new_fw_path" >> "$SO_MAP"
        
        # 特殊处理 .bundle 中的文件:保留符号链接以维持原有导入路径
        if [[ "$so_file" == *".bundle"* ]]; then
            # 计算相对路径并创建符号链接
            local rel_path=$(python3 -c "import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))" "$fw_bin" "$(dirname "$so_file")" 2>/dev/null)
            if [ -n "$rel_path" ]; then
                rm -f "$so_file"
                ln -sf "$rel_path" "$so_file"
            fi
        else
            # 对于 lib-dynload 中的文件,直接删除原文件
            rm -f "$so_file"
        fi
        
        wrapped_count=$((wrapped_count + 1))
    done
    
    echo "  wrapped $wrapped_count .so files into frameworks"
    
    # 如果有新创建的框架,需要重写其他二进制文件中对这些 .so 的引用
    if [ "$wrapped_count" -gt 0 ]; then
        echo "  rewriting references to wrapped .so files..."
        
        # 查找所有可能引用这些 .so 的二进制文件(主可执行文件、所有框架、所有 .dylib 和 .so)
        local binaries_to_check=()
        
        # 主可执行文件
        [ -f "$MAIN_BIN" ] && binaries_to_check+=("$MAIN_BIN")
        
        # 所有框架的可执行文件
        while IFS= read -r -d '' fw; do
            local plist="$fw/Info.plist"
            [ -f "$plist" ] || continue
            local exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
            [ -z "$exe" ] && continue
            local bin="$fw/$exe"
            [ -f "$bin" ] && binaries_to_check+=("$bin")
        done < <(find "$FW" -name "*.framework" -type d -print0 2>/dev/null)
        
        # 所有 .dylib 和 .so 文件
        while IFS= read -r -d '' bin; do
            binaries_to_check+=("$bin")
        done < <(find "$APP" -type f \( -name "*.dylib" -o -name "*.so" \) -print0 2>/dev/null)
        
        # 重写每个二进制中的引用
        for target in "${binaries_to_check[@]}"; do
            [ -f "$target" ] || continue
            while IFS='|' read -r old_name new_load _ _; do
                [ -z "$old_name" ] && continue
                # 尝试多种可能的旧引用路径
                for prefix in "@rpath" "@loader_path/../Frameworks" "@executable_path/Frameworks"; do
                    local old_ref="$prefix/$old_name"
                    if otool -L "$target" 2>/dev/null | grep -q "$old_ref"; then
                        install_name_tool -change "$old_ref" "$new_load" "$target" 2>/dev/null || true
                        break
                    fi
                done
                # 也检查是否引用了 lib-dynload 中的绝对路径
                local dynload_ref="$old_name"
                if otool -L "$target" 2>/dev/null | grep -q "$dynload_ref"; then
                    install_name_tool -change "$dynload_ref" "$new_load" "$target" 2>/dev/null || true
                fi
            done < "$SO_MAP"
        done
    fi
    
    rm -f "$SO_MAP"
    echo "wrap-loose-dylibs: wrapping .so files done"
    

    }

    调用 .so 文件处理函数

    wrap_so_files

    ============================================================

    Step 4: 重新签名所有被修改过的二进制和框架

    ============================================================

    while IFS='|' read -r base new_load fw_dir new_bin; do
    [ -z "$base" ] && continue
    sign_bin "$new_bin"
    codesign --force --sign "$IDENT" --timestamp=none "$fw_dir" 2>/dev/null || true
    done < "$MAP_FILE"

    while IFS= read -r -d '' fw; do
    plist="$fw/Info.plist"
    [ -f "$plist" ] || continue
    exe=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$plist" 2>/dev/null)
    bin="$fw/$exe"
    [ -f "$bin" ] && sign_bin "$bin"
    codesign --force --sign "$IDENT" --timestamp=none "$fw" 2>/dev/null || true
    done < <(find "$FW" -name "*.framework" -type d -print0)

    if [ -n "$APP_PKGS" ]; then
    find "$APP_PKGS" -name "*.so" -type f -not -type l -print0 2>/dev/null |
    while IFS= read -r -d '' so; do
    sign_bin "$so"
    done
    fi

    [ -f "$MAIN_BIN" ] && sign_bin "$MAIN_BIN"

    rm -f "$MAP_FILE"

    LEFT=$(find "$FW" -maxdepth 1 ( -name ".dylib" -o -name ".so" ) -not -type l 2>/dev/null | wc -l | tr -d ' ')
    WRAPPED=$(find "$FW" -name ".framework" -type d | wc -l | tr -d ' ')
    DEEP_LEFT=0
    [ -n "$APP_PKGS" ] && DEEP_LEFT=$(find "$APP_PKGS" -name "
    .dylib" -type f -not -type l 2>/dev/null | wc -l | tr -d ' ')
    STATIC_LEFT=$(find "$FW" "$APP" -name ".a" -type f 2>/dev/null | wc -l | tr -d ' ')
    echo "wrap-loose-dylibs: done — $WRAPPED frameworks, $LEFT loose top-level, $DEEP_LEFT loose in app_packages, $STATIC_LEFT static archives"
    if [ "$LEFT" -gt 0 ] || [ "$DEEP_LEFT" -gt 0 ] || [ "$STATIC_LEFT" -gt 0 ]; then
    echo " ⚠ remaining items the validator may reject:"
    find "$FW" -maxdepth 1 ( -name "
    .dylib" -o -name ".so" ) -not -type l 2>/dev/null | sed 's|^| |'
    [ -n "$APP_PKGS" ] && find "$APP_PKGS" -name "
    .dylib" -type f -not -type l 2>/dev/null | sed 's|^| |'
    find "$FW" "$APP" -name "*.a" -type f 2>/dev/null | sed 's|^| |'
    fi

    ============================================================

    最终 App Store 就绪检查

    ============================================================

    echo "=== wrap-loose-dylibs: final App Store readiness check ==="
    _REJECTED=0
    _check_absent() {
    local label="$1"; shift
    local found=()
    for _path in "$@"; do
    [ -e "$_path" ] && found+=("$_path")
    done
    if [ ${#found[@]} -gt 0 ]; then
    echo " ❌ $label still present:"
    for _f in "${found[@]}"; do echo " $_f"; done
    _REJECTED=$((_REJECTED + ${#found[@]}))
    else
    echo " ✓ $label clean"
    fi
    }
    _static_archives=()
    while IFS= read -r -d '' _a; do _static_archives+=("$_a"); done < <(find "$APP" -name ".a" -type f -print0 2>/dev/null)
    _check_absent "static .a archives" "${_static_archives[@]}"
    _loose_dylibs=()
    while IFS= read -r -d '' _d; do _loose_dylibs+=("$_d"); done < <(find "$FW" -maxdepth 1 -name "
    .dylib" -type f -print0 2>/dev/null)
    _check_absent "loose .dylib in Frameworks/" "${_loose_dylibs[@]}"
    if [ -n "$APP_PKGS" ]; then
    _deep_dylibs=()
    while IFS= read -r -d '' _d; do _deep_dylibs+=("$_d"); done < <(find "$APP_PKGS" -name ".dylib" -type f -not -type l -print0 2>/dev/null)
    _check_absent "loose .dylib in app_packages/" "${_deep_dylibs[@]}"
    fi
    _TORCH_SHM_PATH="$APP/app_packages/site-packages/torch/bin/torch_shm_manager"
    if [ -f "$_TORCH_SHM_PATH" ]; then
    if file "$_TORCH_SHM_PATH" 2>/dev/null | grep -q "Mach-O"; then
    echo " ❌ torch_shm_manager is still a Mach-O — App Store will reject"
    _REJECTED=$((_REJECTED + 1))
    else
    echo " ✓ torch_shm_manager is text placeholder (App Store-safe)"
    fi
    else
    echo " ⚠ torch_shm_manager missing — torch.multiprocessing won't initialize"
    fi
    _check_absent "manimpango native .so (CTFontCopyDefaultCascadeList)"
    "$APP/app_packages/site-packages/manimpango/_register_font.cpython-314-iphoneos.so"
    "$APP/app_packages/site-packages/manimpango/cmanimpango.cpython-314-iphoneos.so"
    _check_absent "psutil._psutil_osx (IOPSCopyPowerSources
    )"
    "$APP/app_packages/site-packages/psutil/_psutil_osx.abi3.so"
    [ -d "$FW/latex" ] && [ ! -d "$FW/latex.framework" ] &&
    echo " ❌ Frameworks/latex/ data dir still present" && _REJECTED=$((_REJECTED + 1))
    [ -d "$FW/katex" ] && [ ! -d "$FW/katex.framework" ] &&
    echo " ❌ Frameworks/katex/ data dir still present" && _REJECTED=$((_REJECTED + 1))

    if [ "$_REJECTED" -gt 0 ]; then
    echo ""
    echo "❌ App Store readiness FAILED: $_REJECTED rejected file(s) still in bundle."
    echo " App Store Connect will reject this upload. Fix above issues before archiving."
    exit 1
    fi
    echo "✓ App Store readiness OK"
    exit 0
    打包上传,新的app freamework产生了很多小文件如下图,但是包中的.so文件没有删除

    Image

    还是报 code = 90171;错误,在这个新的脚本中提交一次审核报了 code = 90210;
    Invalid bundle structure. The “CustomCalculator.app/python-ios-lib_SciPy.bundle/scipy/linalg/_fblas.cpython-314-iphoneos.so” binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
    The server’s response was: ‘{
    code = 90171;
    description = "Invalid bundle structure. The \U201cCustomCalculator.app/python-ios-lib_SciPy.bundle/scipy/linalg/_fblas.cpython-314-iphoneos.so\U201d binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle";
    }’.

    Missing load commands. The executable at 'CustomCalculator.app/Frameworks/numpy.framework' does not have the necessary load commands. Try rebuilding the app with the latest Xcode version. If you are using third party development tools, contact the provider.
    The server’s response was: ‘{
    code = 90210;
    description = "Missing load commands. The executable at 'CustomCalculator.app/Frameworks/numpy.framework' does not have the necessary load commands. Try rebuilding the app with the latest Xcode version. If you are using third party development tools, contact the provider.";
    }’.

  9. yu314-coder commented on Jun 2, 2026

    @yu314-coder
    Owner

    Thanks for the very detailed write-up and screenshots 🙏 Four points:

    1) scipy _dcabs1_ — resolved. Good.

    2) blas_compat_stubs.c build failure ("Could not build module 'Foundation'/'UIKit'", "Module 'ObjectiveC.NSObject' requires feature 'objc'"): that's the .c file inheriting your app's Objective-C prefix header — a pure-C translation unit can't build the ObjC system modules. Fix: rename it to blas_compat_stubs.m (Objective-C is a superset of C, so the stubs still compile), or exclude it from the prefix header. I've updated the README example to .m.

    3) code = 90171 (loose .so "binary file is not permitted"). Your diagnosis is exactly right — App Store requires every Mach-O to live inside a .framework under Frameworks/. The issue is the SwiftPM resource-bundle layout: the .so in python-stdlib/lib-dynload/ and inside python-ios-lib_*.bundle/ are loose, and the script I'd pointed you at (wrap-loose-dylibs.sh) only covers the app_packages/ layout — that was a documentation mistake on my side.

    The repo already ships the script built for exactly this layout:

    • scripts/appstore/wrap-binaries-as-frameworks.sh
    • scripts/appstore/python_ios_lib_import_hook.py

    It wraps every .so in lib-dynload/ and the bundles into Frameworks/*.framework, rewrites the cross-LC_LOAD_DYLIB references, re-signs, and a runtime import hook (MetaPathFinder) re-routes import numpy._core… to the framework — i.e. the same thing your manually-extended script does. I had wrongly marked it "deprecated"; I've corrected the README and added an App Store Connect submission section with the full SwiftPM steps (build phase + import-hook bootstrap):

    https://github.com/yu314-coder/python-ios-lib#app-store-connect-submission

    4) "Upload Symbols Failed: no dSYM for Python.framework" is a non-blocking warning, not a rejection — the prebuilt Python.framework ships without a dSYM. Untick "Upload your app's symbols" in the Distribute flow, or ignore it.

    Thanks again for the thorough repro — it caught a genuinely misleading bit in the docs.

  10. baixiaochunbudanchun commented on Jun 3, 2026

    @baixiaochunbudanchun
    Author

    作者你好,再次感谢你的回复及技术指导,我根据你的README.md文档,使用Swift Package Manager这种方式,对项目进行修改,使用blas_compat_stubs.m方式替换libscipy_blas_stubs.dylib文件,同时我还执行了wrap-binaries-as-frameworks.sh脚本,然后项目在debug环境报如下错误。

    Image

    Archive 打好的包,提交商店审核,依然被拒,产生下面三种类型的Code,
    Invalid bundle structure. The “CustomCalculator.app/Frameworks/scipy-optimize-minpack.framework/scipy-optimize-minpack” binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
    The server’s response was: ‘{
    code = 90171;
    description = "Invalid bundle structure. The \U201cCustomCalculator.app/Frameworks/scipy-optimize-minpack.framework/scipy-optimize-minpack\U201d binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle";
    }’.

    The binary is invalid. The executable 'CustomCalculator.app/Frameworks/scipy-interpolate-rgi-cython.framework/scipy-interpolate-rgi-cython' has type 'BUNDLE' that is not valid. Only 'EXECUTE' is permitted.
    The server’s response was: ‘{
    code = 90124;
    description = "The binary is invalid. The executable 'CustomCalculator.app/Frameworks/scipy-interpolate-rgi-cython.framework/scipy-interpolate-rgi-cython' has type 'BUNDLE' that is not valid. Only 'EXECUTE' is permitted.";
    }’.

    Missing load commands. The executable at 'CustomCalculator.app/Frameworks/scipy-sparse-csgraph-traversal.framework' does not have the necessary load commands. Try rebuilding the app with the latest Xcode version. If you are using third party development tools, contact the provider.
    The server’s response was: ‘{
    code = 90210;
    description = "Missing load commands. The executable at 'CustomCalculator.app/Frameworks/scipy-sparse-csgraph-traversal.framework' does not have the necessary load commands. Try rebuilding the app with the latest Xcode version. If you are using third party development tools, contact the provider.";
    }’.

    同时我还尝试使用python_ios_lib_import_hook.py脚本,但是失败,下图是我的使用方式

    Image

    我还尝试先后同时运行wrap-loose-dylibs.sh 和wrap-binaries-as-frameworks.sh 脚本,打包上传应用上商店,还是被拒

    还请你在项目中写一个完整的可以审核通过的 xcode OC demo让我学习,我的项目目前只导入numpy和scipy,
    再次感谢你的回复与技术指导

  11. baixiaochunbudanchun commented on Jun 3, 2026

    @baixiaochunbudanchun
    Author

    Run Script Wrap-binaries-as-frameworks 下面内容是我执行wrap-binaries-as-frameworks.sh的方式

    #!/bin/bash
    set -e
    WRAP="${SRCROOT}/scripts/wrap-binaries-as-frameworks.sh"
    if [ ! -f "$WRAP" ]; then
    echo "error: wrap-binaries-as-frameworks.sh not found at $WRAP"
    exit 1
    fi

    if [ -f "$WRAP" ]; then
    bash "$WRAP"
    fi

    3. 修复所有生成的 .framework 的 CFBundleIdentifier

    APP_BUNDLE_ID="${PRODUCT_BUNDLE_IDENTIFIER}" # 从 Xcode 环境自动获取
    FRAMEWORKS_DIR="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
    find "$FRAMEWORKS_DIR" -name ".framework" -type d | while read fw; do
    plist="$fw/Info.plist"
    if [ -f "$plist" ]; then
    old_id=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$plist" 2>/dev/null)
    # 只修改由 wrap 脚本生成的标识符(包含 ai.codebench.pythonext)
    if [[ "$old_id" == ai.codebench.pythonext.
    ]]; then
    # 新标识符:主BundleID.pythonext.原框架名
    fw_name=$(basename "$fw" .framework)
    new_id="${APP_BUNDLE_ID}.pythonext.${fw_name}"
    /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $new_id" "$plist"
    echo "Fixed CFBundleIdentifier for $fw: $old_id -> $new_id"
    # 重新签名(因为修改了 plist)
    codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY:--}" "$fw"
    fi
    fi
    done

  12. baixiaochunbudanchun commented on Jun 3, 2026

    @baixiaochunbudanchun
    Author

    Invalid bundle structure. The “CustomCalculator.app/Frameworks/scipy-sparse-csgraph-flow.framework/scipy-sparse-csgraph-flow” binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
    The server’s response was: ‘{
    code = 90171;
    description = "Invalid bundle structure. The \U201cCustomCalculator.app/Frameworks/scipy-sparse-csgraph-flow.framework/scipy-sparse-csgraph-flow\U201d binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle";
    }’.针对这个错,添加上我应用包里面的文件结构如下

    Image
  13. yu314-coder commented on Jun 3, 2026

    @yu314-coder
    Owner

    Thanks for the detailed repro and screenshots again — they pinpoint it exactly. All three rejection codes are the same root cause, and it was a real gap in the script I pointed you to. Apologies — that one's on me.

    Root cause: MH_BUNDLE vs MH_DYLIB. Python .so extension modules are Mach-O type MH_BUNDLE (filetype 0x8, dlopen-only). Apple's validator rejects any .framework whose executable is MH_BUNDLE:

    • 90124 — "has type 'BUNDLE'… Only 'EXECUTE' is permitted" → must be MH_DYLIB
    • 90210 — "missing load commands" → MH_BUNDLE has no LC_ID_DYLIB
    • 90171 — "standalone library… not permitted" → the malformed (non-dylib) binary

    wrap-binaries-as-frameworks.sh wrapped your .so into .frameworks but didn't flip the Mach-O type — its install_name_tool -id silently no-ops on an MH_BUNDLE, so they shipped as BUNDLE. The repo already had the fixer (scripts/appstore/fix-macho-type.py), but the wrap script wasn't calling it. Fixed now (commit 321213f5) — the wrap script runs the flip automatically before re-signing.

    To unblock you today, either is enough:

    Option A — pull the updated script (scripts/appstore/wrap-binaries-as-frameworks.sh, now self-contained; keep fix-macho-type.py beside it), then re-archive.

    Option B — add one Run Script phase right after your wrap phase (and before Xcode's code-sign):

    python3 "${SRCROOT}/scripts/appstore/fix-macho-type.py" "${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}"

    Verify before archiving — this should print DYLIB, not BUNDLE:

    otool -hv "$BUILT_PRODUCTS_DIR/.../Frameworks/scipy-optimize-minpack.framework/scipy-optimize-minpack" | grep -A1 filetype

    A few more notes:

    • Order matters with your custom CFBundleIdentifier step: run wrap → fix-macho-type → your plist fix + codesign. The type-flip mutates the binary, so it must precede the final signature.
    • Don't run both wrap-loose-dylibs.sh and wrap-binaries-as-frameworks.sh. For a SwiftPM app use only wrap-binaries-as-frameworks.sh.
    • Import hook: install it after Py_Initialize() and before the first import numpy, with PYTHON_IOS_LIB_HOOK_DIR pointing at your bundled python-stdlib. If it still fails, paste the exact traceback and I'll look.

    If an archive still gets a code after the type-flip, send the otool -hv of the offending framework binary and I'll pinpoint it.

  14. baixiaochunbudanchun commented on Jun 4, 2026

    @baixiaochunbudanchun
    Author

    你好,非常感谢你的耐心解答和指导,经过反复的尝试我已经将应用打包到商店并成功提交审核,这个过程还是有许多细节问题,后面我会总结一下反馈给你,但是我也收到一个新的商店反馈问题。由于时间我还没有看你的readme文档是否有该解决方案,图片中拒绝APP上架的原因

    Image
  15. yu314-coder commented on Jun 4, 2026

    @yu314-coder
    Owner

    🎉 Congrats — getting into review means the MH_BUNDLE → MH_DYLIB flip cleared all three codes (90124/90210/90171). And yes, please send that write-up of the detail issues — I'll fold them into the README so the next person hits fewer walls.

    ITMS-91061 is a separate, newer requirement, not a regression. Your _ssl and _hashlib modules statically link OpenSSL/BoringSSL, which is on Apple's commonly-used third-party SDK list. Since spring 2024, any binary bundling such an SDK must ship a PrivacyInfo.xcprivacy — so the wrapped stdlib_ssl.framework / stdlib_hashlib.framework each need one.

    Fix — a minimal manifest (these do crypto only: no tracking, no data collection):

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0"><dict>
      <key>NSPrivacyTracking</key><false/>
      <key>NSPrivacyTrackingDomains</key><array/>
      <key>NSPrivacyCollectedDataTypes</key><array/>
      <key>NSPrivacyAccessedAPITypes</key><array/>
    </dict></plist>

    It must sit at each framework root inside the codesign seal (add it before signing): stdlib_ssl.framework/PrivacyInfo.xcprivacy, stdlib_hashlib.framework/PrivacyInfo.xcprivacy.

    I've automated this in the repo (commit 0ed7c8ed): scripts/appstore/PrivacyInfo.xcprivacy + wrap-binaries-as-frameworks.sh now copies it into every OpenSSL-linked wrapped framework right before re-signing. So pull the update and re-archive — no manual step.

    Two notes:

    • Your app also needs its own top-level PrivacyInfo.xcprivacy (in the app target) declaring your app's data use + any required-reason APIs your own code uses (e.g. UserDefaults → CA92.1). That's separate from these SDK manifests.
    • If a later upload returns ITMS-91053 ("missing required-reason API") for OpenSSL, send the exact API it names and I'll add the reason code.

    Verify before upload: unzip -l YourApp.ipa | grep xcprivacy should list the manifests inside the frameworks.

  16. baixiaochunbudanchun commented on Jun 5, 2026

    @baixiaochunbudanchun
    Author

    此次回复主要感谢作者的耐心回复和精准的技术指导,解决我遇到的问题。下面我将遇到的问题、解决方法和注意事项详细列举
    此次集成主要使用SPM(Swift Package Manager实现framework自动管理和分发)引入库,在三个阶段出现的问题如下(我在最后附加上我的项目配置和最终执行的文件):
    第一阶段项目运行wrap-binaries-as-frameworks.sh进行编译报的错误
    1.没有导出 dcabs1 / scabs1;而 cython_blas 是在 flat namespace(平面命名空间) 里查找它们的,所以真机上 dyld 一旦找不到 dcabs1 就崩溃

    2.应用编译不通过
    dyld[33098]: Library not loaded: @rpath/libfortran_io_stubs.dylib Referenced from: <3FB752A6-86D3-388F-AA6B-54F8425DE8DE> /private/var/containers/Bundle/Application/A05662E9-EEA6-4944-B901-3789FBBC7178/xxxx.app/xxxx
    Library not loaded: @rpath/libfortran_io_stubs.dylib Referenced from: <3FB752A6-86D3-388F-AA6B-54F8425DE8DE> /private/var/containers/Bundle/Application/A05662E9-EEA6-4944-B901-3789FBBC7178/xxxx.app/xxxx

    rewrite_cross_refs 函数在处理 @rpath/xxx.dylib 形式的依赖时,匹配逻辑不够强,导致引用未被更新,在我的wrap-binaries-as-frameworks.sh有修复

    3.应用编译通过,python_ios_lib_import_hook.py 调用失败
    Application/40902EC6-3799-4F4A-A46B-EF8E1C5AB87B/CustomCalculator.app/python-stdlib
    Traceback (most recent call last): File "", line 5, in File "/private/var/containers/Bundle/Application/40902EC6-3799-4F4A-A46B-EF8E1C5AB87B/CustomCalculator.app/python-stdlib/python_ios_lib_import_hook.py", line 27 import sys, os IndentationError: unexpected indent

    第二阶段应用提交审核
    1应用包提交商店前出现错误(code值包括:90530、90360、90171、90124、90210)
    Invalid MinimumOSVersion. Apps that only support 64-bit devices must specify a deployment target of 8.0 or later. MinimumOSVersion in 'xxxx.app/Frameworks/scipy.sparse.linalg._propack._spropack.framework' is ''.
    The server’s response was: ‘{
        code = 90530;
        description = "Invalid MinimumOSVersion. Apps that only support 64-bit devices must specify a deployment target of 8.0 or later. MinimumOSVersion in 'xxxx.app/Frameworks/scipy.sparse.linalg._propack._spropack.framework' is ''.";
    }’.

    Missing Info.plist value. A value for the key 'MinimumOSVersion' in bundle xxxx.app/Frameworks/_interpqueues.framework is required.
    The server’s response was: ‘{
        code = 90360;
        description = "Missing Info.plist value. A value for the key 'MinimumOSVersion' in bundle hLifeKup.app/Frameworks/_interpqueues.framework is required.";
    }’.

    Invalid bundle structure. The “hLifeKup.app/Frameworks/scipy.optimize._highspy._core.framework/scipy.optimize._highspy._core” binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle
    The server’s response was: ‘{
        code = 90171;
        description = "Invalid bundle structure. The \xxxx.app/Frameworks/scipy.optimize._highspy._core.framework/scipy.optimize._highspy._core\U201d binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle";
    }’.

    2应用包提交到商店,在TestFlight 版本构建失败,包上传成功到构建失败时间间隔在8分钟左右会发失败邮件提醒
    ITMS-90338: Non-public API usage - The app references non-public symbols in Frameworks/scipy_linalg_cython_lapack.framework/scipy_linalg_cython_lapack: xerbla_array_. If method names in your source code match the private Apple APIs listed above, altering your method names will help prevent this app from being flagged in future submissions. In addition, note that one or more of the above APIs may be located in a static library that was included with your app. If so, they must be removed. For further information, visit the Technical Support Information at http://developer.apple.com/support/technical/
    ITMS-90208: Invalid Bundle - The bundle hLifeKup.app/Frameworks/loose_libscipy_blas_stubs.framework does not support the minimum OS Version specified in the Info.plist.
    ITMS-90338和ITMS-90208 READMe文档有提到解决方法,
    1)在wrap-loose-dylibs.sh 和patch-cython-lapack.py 解决ITMS-90338 方案,后面我在我使用的wrap-binaries-as-frameworks.sh 也会加上解决ITMS-90338的方法。
    2)ITMS-90208 (细节问题)主要是设置应用适用最低版本问题,在项目的TARFETS->General->Minimun Deployments iOS ^17.0 (大于17以上版本号即可)

    第三阶段审核中拒绝应用上架
    ITMS-91061: Missing privacy manifest - Your app includes “Frameworks/stdlib_hashlib.framework/stdlib_hashlib”, which includes BoringSSL / openssl_grpc, an SDK that was identified in the documentation as a commonly used third-party SDK. If a new app includes a commonly used third-party SDK, or an app update adds a new commonly used third-party SDK, the SDK must include a privacy manifest file. Please contact the provider of the SDK that includes this file to get an updated SDK version with a privacy manifest. For more details about this policy, including a list of SDKs that are required to include signatures and manifests, visit: https://developer.apple.com/support/third-party-SDK-requirements

    解决方发:
    隐私清单(ITMS-91061)。 Python 的 _ssl / _hashlib 静态链接了 OpenSSL/BoringSSL,这是一个“常用的第三方 SDK” — 因此包装后的 stdlib_ssl.framework / stdlib_hashlib.framework 必须各自携带一个 PrivacyInfo.xcprivacy,否则 App Store Connect 会以 "Missing privacy manifest"(ITMS-91061)拒绝。包装脚本会将位于 scripts/appstore/PrivacyInfo.xcprivacy 的最小清单(无追踪、无数据收集)复制到每个链接了 OpenSSL 的 framework 中,然后重新签名 — 请将该文件放在脚本旁边。如果后续上传返回 ITMS-91053("missing required-reason API"),请将指定的原因代码添加到清单的 NSPrivacyAccessedAPITypes 数组中。

    在wrap-binaries-as-frameworks.sh 的这个# =====================================================

    4. Pass 3: re-sign every framework now that install_name_tool changes

    位置上面添加下面代码

    ============================================================

    3c. Privacy manifests (ITMS-91061). Any binary bundling a "commonly used

    third-party SDK" must ship a PrivacyInfo.xcprivacy or App Store Connect

    rejects. OpenSSL/BoringSSL is on that list and is statically linked into

    Python's _ssl / _hashlib (→ stdlib_ssl/stdlib_hashlib.framework). Drop a

    minimal manifest (no tracking, no data collection) into every wrapped

    framework whose binary carries OpenSSL markers — BEFORE the re-sign below,

    so codesign seals it into the bundle.

    ============================================================

    PRIVACY_SRC=""
    for cand in "$SCRIPT_DIR/PrivacyInfo.xcprivacy"
    "${SRCROOT}/scripts/appstore/PrivacyInfo.xcprivacy"
    "${SRCROOT}/scripts/PrivacyInfo.xcprivacy"; do
    [ -f "$cand" ] && { PRIVACY_SRC="$cand"; break; }
    done
    if [ -n "$PRIVACY_SRC" ]; then
    while IFS= read -r -d '' fw; do
    fname=$(basename "$fw" .framework)
    bin="$fw/$fname"
    [ -f "$bin" ] || continue
    if strings "$bin" 2>/dev/null | grep -qiE 'BoringSSL|openssl_grpc|OpenSSL [0-9]+.'
    || echo "$fname" | grep -qiE '(ssl|hashlib|crypto)'; then
    cp "$PRIVACY_SRC" "$fw/PrivacyInfo.xcprivacy"
    echo "wrap-binaries: privacy manifest → $fname.framework (OpenSSL — ITMS-91061)"
    fi
    done < <(find "$FRAMEWORKS" -name "*.framework" -type d -print0)
    else
    echo "warning: PrivacyInfo.xcprivacy not found beside this script or under $SRCROOT/scripts[/appstore] —"
    echo " OpenSSL-linked frameworks (stdlib_ssl/stdlib_hashlib) will be rejected with ITMS-91061."
    fi

    =============我的项目代码及文件结构仅供参考==============

    第一在OC项目中,下面是我初始化Python 的环境以及引入python_ios_lib_import_hook.py(注意脚本开始的脚本介绍要删除掉,否则会被程序执行然后报错)脚本代码

    • (void)setupPythonEnvironment {
          NSLog(@"🚀 [PyInit] 开始配置 Python 环境...");
          NSBundle *mainBundle = [NSBundle mainBundle];
          NSURL *bundleURL = [mainBundle bundleURL];
          NSURL *stdlib = [bundleURL URLByAppendingPathComponent:@"python-stdlib"];
          NSURL dynload = [stdlib URLByAppendingPathComponent:@"lib-dynload"];
          // 自动收集所有 python-ios-lib_
      .bundle
          NSMutableArray *libBundles = [NSMutableArray array];
          NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:bundleURL.path error:nil];
          for (NSString *name in contents) {
              if ([name hasPrefix:@"python-ios-lib_"] && [name hasSuffix:@".bundle"]) {
                  [libBundles addObject:[[bundleURL URLByAppendingPathComponent:name] path]];
              }
          }
          NSMutableString *pythonPath = [NSMutableString stringWithFormat:@"%@:%@", stdlib.path, dynload.path];
          for (NSString *bundlePath in libBundles) {
              [pythonPath appendFormat:@":%@", bundlePath];
          }
          setenv("PYTHONHOME", stdlib.path.UTF8String, 1);
          setenv("PYTHONPATH", pythonPath.UTF8String, 1);
          setenv("PYTHONNOUSERSITE", "1", 1);
          setenv("PYTHONDONTWRITEBYTECODE", "1", 1);
          setenv("PYTHONMALLOC", "malloc", 1);   
          setenv("TMPDIR", NSTemporaryDirectory().UTF8String, 1);
          
          // 根据真机/模拟器选择正确的 sysconfigdata
          // 真机 arm64:
          setenv("_PYTHON_SYSCONFIGDATA_NAME", "_sysconfigdata__ios_arm64-iphoneos", 1);
          // 若为模拟器则改为 "_sysconfigdata__ios_arm64-iphonesimulator"
          setenv("PYTHON_IOS_LIB_HOOK_DIR",stdlib.path.UTF8String, 1);
          setenv("PYTHONPATH", [pythonPath UTF8String], 1);   // 可选,但建议
          //3. 初始化解释器 ---
          if (!Py_IsInitialized()) {
              Py_Initialize();
              
              NSLog(@"✅ [PyInit] Python 解释器已初始化");
              const char *pythonScript = "print('Python environment is ready!')";
              PyRun_SimpleString(pythonScript);
              //    新添加的
              NSLog(@"安装运行时 import hook的路径 stdlib path: %@", stdlib.path);
              setenv("PYTHON_IOS_LIB_HOOK_DIR", stdlib.path.UTF8String, 1);
              
              // 安装 import hook   下面格式不要动,否则影响钩子初始化
              PyRun_SimpleString(
                  "import sys, os\n"
                  "hook_dir = os.environ.get('PYTHON_IOS_LIB_HOOK_DIR')\n"
                  "if hook_dir and os.path.isdir(hook_dir):\n"
                  "    sys.path.insert(0, hook_dir)\n"
                  "    import python_ios_lib_import_hook\n"
                  "    print('🚗DEBUG: PYTHONHOME =', os.environ.get('PYTHONHOME'))\n"
                  "    home = os.environ.get('PYTHONHOME')\n"
                  "    bundle = os.path.dirname(home) if home else None\n"
                  "    print('🚗DEBUG: bundle =', bundle)\n"
                  "    manifest = os.path.join(bundle, 'python-ios-lib_extension_manifest.txt') if bundle else None\n"
                  "    print('🚗DEBUG: manifest path =', manifest)\n"
                  "    print('🚗DEBUG: manifest exists =', os.path.exists(manifest) if manifest else False)\n"
                  "    python_ios_lib_import_hook.install()\n"
                  "    print('🚗DEBUG: _INSTALLED after install =', python_ios_lib_import_hook._INSTALLED)\n"
                  "else:\n"
                  "    print('Hook dir invalid')\n"
              );
          }
      }
      项目和app包文件结构如下图:
    Image

    ### Sign Python Binary Modules如下
    #!/bin/bash
    set -e
    BEEWARE="${SRCROOT}/Support/Python.xcframework"
    PYIOSLIB_FW="${SRCROOT}/Support/Frameworks"   # adjust if needed
    APP="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}"
    DST="$APP/python-stdlib"
    FRAMEWORKS="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}"
    IDENT="${EXPANDED_CODE_SIGN_IDENTITY:--}"

    case "$PLATFORM_NAME" in
      iphoneos)        SLICE="ios-arm64" ;;
      iphonesimulator) SLICE="ios-arm64_x86_64-simulator" ;;
    esac
    ARCH="${CURRENT_ARCH:-arm64}"; [ "$ARCH" = "undefined_arch" ] && ARCH="arm64"

    1. Copy stdlib + per-arch lib-dynload + sysconfigdata.

    mkdir -p "$DST"
    rsync -a --delete --exclude 'pycache' --exclude 'lib-dynload'
      "$BEEWARE/lib/python3.14/" "$DST/"
    rsync -a --delete
      "$BEEWARE/$SLICE/lib-$ARCH/python3.14/lib-dynload/" "$DST/lib-dynload/"
    find "$BEEWARE/$SLICE/lib-$ARCH/python3.14" -maxdepth 1
      -name "sysconfigdata_*.py" -exec cp -f {} "$DST/" ;

    2. Copy standalone dylibs from python-ios-lib/Frameworks/ into app

    Frameworks/ (libfortran_io_stubs.dylib for scipy.special, ffmpeg dylibs

    for PyAV, etc.).

    mkdir -p "$FRAMEWORKS"
    if [ -d "$PYIOSLIB_FW" ]; then
      while IFS= read -r -d '' src; do
        base=$(basename "$src")
        cp -f "$src" "$FRAMEWORKS/$base"
        codesign --force --sign "$IDENT" --timestamp=none "$FRAMEWORKS/$base" 2>/dev/null || true
      done < <(find "$PYIOSLIB_FW" -maxdepth 2 -name "*.dylib" -print0)
    fi

    2.1 复制 import hook 到 python-stdlib/

    HOOK_SRC="${SRCROOT}/Support/Frameworks/python_ios_lib_import_hook.py"
    if [ -f "$HOOK_SRC" ]; then
        cp -f "$HOOK_SRC" "$DST/"
        echo "Copied python_ios_lib_import_hook.py to $DST"
    else
        echo "warning: python_ios_lib_import_hook.py not found at $HOOK_SRC"
    fi

    3. Re-sign every .so/.dylib in stdlib + python-ios-lib_*.bundle.

    sign_lib() {
      codesign --force --sign "$IDENT" --timestamp=none
        --preserve-metadata=identifier,entitlements,flags "$1" 2>/dev/null
        || codesign --force --sign "$IDENT" --timestamp=none "$1"
    }
    find "$DST/lib-dynload" -name ".so" -print0 | while IFS= read -r -d '' so; do sign_lib "$so"; done
    for b in "$APP"/python-ios-lib_
    .bundle; do
      [ -d "$b" ] || continue
      while IFS= read -r -d '' lib; do sign_lib "$lib"; done
        < <(find "$b" ( -name ".so" -o -name ".dylib" ) -print0)
    done

    4. Rewrite PyAV's hardcoded /tmp/ffmpeg-ios/ install_names → @rpath.

    AV="$APP/python-ios-lib_FFmpegPyAV.bundle"
    fix_names() {
      for old in $(otool -L "$1" 2>/dev/null | awk '//tmp/ffmpeg-ios/{print $1}'); do
        install_name_tool -change "$old" "@rpath/$(basename "$old")" "$1" 2>/dev/null || true
      done
      for old in $(otool -D "$1" 2>/dev/null | awk 'NR==2 && //tmp/ffmpeg-ios/{print $1}'); do
        install_name_tool -id "@rpath/$(basename "$old")" "$1" 2>/dev/null || true
      done
    }
    for lib in "$FRAMEWORKS"/libav*.dylib "$FRAMEWORKS"/libsw*.dylib; do
      [ -f "$lib" ] && { fix_names "$lib"; sign_lib "$lib"; }
    done
    if [ -d "$AV/av" ]; then
      while IFS= read -r -d '' so; do fix_names "$so"; sign_lib "$so"; done
        < <(find "$AV/av" -name "*.so" -print0)
    fi
    Run Script Wrap-binaries-as-frameworks 如下:
    set -e
    WRAP="${SRCROOT}/scripts/wrap-binaries-as-frameworks.sh"
    if [ ! -f "$WRAP" ]; then
        echo "error: wrap-binaries-as-frameworks.sh not found at $WRAP"
        exit 1
    fi
    bash "$WRAP"

    Run Script Wrap-binaries-as-frameworks 如下:
    set -e
    WRAP="${SRCROOT}/scripts/wrap-binaries-as-frameworks.sh"
    if [ ! -f "$WRAP" ]; then
    echo "error: wrap-binaries-as-frameworks.sh not found at $WRAP"
    exit 1
    fi
    bash "$WRAP"

  17. added a commit that references this issue on Jun 5, 2026
  18. yu314-coder commented on Jun 5, 2026

    @yu314-coder
    Owner

    Huge thanks for this — it's one of the best closeout write-ups I've received, and exactly what I hoped for when I asked. Congrats on getting through review. 🎉

    I've folded the two net-new items from your report into the repo (commit 0217addb):

    1. Library not loaded: @rpath/libfortran_io_stubs.dylib (your Stage 1.2). You're right that rewrite_cross_refs could miss a bare @rpath/<basename>.dylib dep. I've added an on-disk fallback: when a dependency matches no RPATH_MAP entry, the script now probes Frameworks/ directly for the wrapped loose_<stem> / <stem> framework and rewrites the reference to it, instead of leaving @rpath/<lib>.dylib dangling → the runtime crash. It's purely additive — it only fires when the reference is otherwise unmatched and the wrapped framework actually exists on disk, so it can't regress existing matches.

    If your own patch to rewrite_cross_refs took a different angle, please paste the diff — I'd rather merge your exact, device-tested version than my reconstruction of it.

    2. python_ios_lib_import_hook.py → IndentationError: unexpected indent at ~line 27 (your Stage 1.3). Nailed it — line 27 is the indented usage example inside the file's leading docstring. If the """ delimiters get dropped during a copy, that example parses as top-level code → the exact error you saw. Documented now: copy the file verbatim (or just delete the leading docstring — it's only documentation), and install via the \n-joined form rather than an indented """…""" heredoc:

    PyRun_SimpleString(
        "import sys, os\n"
        "sys.path.insert(0, os.environ['PYTHON_IOS_LIB_HOOK_DIR'])\n"
        "import python_ios_lib_import_hook\n"
        "python_ios_lib_import_hook.install()\n");

    Both are now in the Common gotchas table, with your report credited.

    On the rest of your write-up — all already handled, so a fresh pull covers them:

    • Privacy manifest (ITMS-91061) — your insertion point is correct; this is already automated in the wrap script (commit 0ed7c8ed): it drops scripts/appstore/PrivacyInfo.xcprivacy into every OpenSSL-linked framework before the re-sign pass.
    • MinimumOSVersion (90530 / 90360) + bundle structure (90171) + ITMS-90338 / 90208 — covered by the wrap script's write_plist (it stamps MinimumOSVersion = your IPHONEOS_DEPLOYMENT_TARGET) + the MH_BUNDLE → MH_DYLIB flip + wrap-loose-dylibs.sh / patch-cython-lapack.py. Setting the deployment target to ≥ iOS 17 as you did is the right call.
    • dcabs1 / scabs1 flat-namespace crash — that's the libscipy_blas_stubs.dylib path (build-blas-stubs.sh), also in the gotchas table.

    Thanks again for documenting all three stages so thoroughly — it'll save the next person a lot of walls. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions