macOS 앱에 Python 런타임과 pip 패키지 완전히 내장하기
안녕하세요. 하늘선입니다.
블로그로는 꽤 오랜만에 인사드리는 것 같습니다.
오늘은 독립된 Mac앱에서 python 및 pip 패키지를 번들링 하는 법에 대해 기록을 남겨보고자 합니다.
원래는 iOS도 같이 준비하려고 했으나 글이 길어지고 복잡해질 우려도 있고, 제가 조금 더 검증하고 같이 전달드리고 싶어서 macOS용으로 글을 먼저 쓰게 되었습니다.
먼저 이 글은 다수의 도전적인 실험을 바탕으로 남긴 내용이며, 잘못된 포함되어 있을 수 있다는 말을 미리 남기고 싶습니다.
의견을 댓글로 달아주시면 좋겠습니다 :)
0. 이 방법을 찾게 된 계기
먼저 이 방법을 찾게 된 계기부터 설명을 드리고 싶은데,
이는 프로젝트 Applepie-RPC를 만들기 위함이었습니다.
이 프로젝트는 아래 사진과 같이 디스코드에 애플뮤직의 현재 음악 듣는 중 상태를 띄워주는 프로젝트입니다.
물론, 기존에도 애플뮤직의 상태를 디스코드에 띄워주는 프로그램은
여럿 존재했었습니다.
그럼 이 프로젝트의 차별점 또한 설명드려야겠죠.
이 프로젝트는 무려 같은 네트워크상의 홈팟, 애플티비를 조회하여 그 음악 정보까지
디스코드에 띄워줄 수 있습니다.
이 방식은 전 세계 최초이며, 아직 구현한 사람이 없었기에 직접 쓰고 싶어서 만들게 되었습니다.
이런식으로 디바이스를 선택해서 선택된 디바이스에 대한 정보값을 가져오고,
그 정보를 바탕으로 디스코드 api를 호출하여 정보를 업데이트 합니다.
하지만 이 프로젝트를 진행하면서 여러 어려움을 겪게 됩니다.
1. 애플티비나 홈팟의 정보를 가져올 Swift 라이브러리의 부재
2. 디스코드에 정보를 제공하기 위해 사용할 "Discord rich presense"의 swift 라이브러리의 방치
1번의 경우 python은 pyatv라는 python 패키지가 존재하나, Swift에서 사용하기 위해서는 홈팟의 연결 핸드쉐이킹부터 수많은 양의 프로토콜을 준수한 연결부와 인증과정을 구현해야 했습니다.
https://github.com/postlund/pyatv
GitHub - postlund/pyatv: A client library for Apple TV and AirPlay devices
A client library for Apple TV and AirPlay devices. Contribute to postlund/pyatv development by creating an account on GitHub.
github.com
이 레포지토리에서 알 수 있듯 이는 많은 양의 코드를 다시 써야하고, 만든다고 해서 업데이트 시 유지보수가 가능하다고 생각하지 않았기에 Swift용 레포를 따로 파는 것은 불가능 하다고 생각했습니다.
2번도 문제였는데, python기반 discord rich presense는 아직 업데이트 되긴 하지만, Swift용의 discord rich presense는 이미 아카이브가 된 후였고, 업데이트가 끊긴 지 오래 되었었습니다.
https://github.com/Azoy/SwordRPC
GitHub - Azoy/SwordRPC: A Discord Rich Presence Library for Swift
A Discord Rich Presence Library for Swift. Contribute to Azoy/SwordRPC development by creating an account on GitHub.
github.com
여기서 저는 이 두 가지 문제점을 한번에 해결하기 위해서 Swift에서 파이썬을 네이티브로 쓰는 것이 가장 좋은 해결 방식이라 생각했고,
이를 구현하기 위해 여러가지 자료를 찾아보았습니다.
일단 파이썬을 Swift에서 사용하기 위해서는 대표적으로 PythonKit을 사용할 수 있는데, 이는 파이썬을 불러와서 사용할 수 있게 해주는 브릿징 킷일 뿐이지 파이썬 런타임이 내장되어 있진 않습니다.
그렇기에 iOS 같은 파이썬 런타임이 내장되어 있지 않은 경우 에러가 날 수 있습니다.
1. 파이썬 런타임을 Swift 앱에 내장 시키기
즉 그렇다는 이야기는 파이썬 런타임을 같이 내장시켜 준다면 Mac 어플리케이션 단독, 심지어 추후 나아가 iOS 환경에서도 원활하게 PyhtonKit을 사용할 수 있다는 이야기가 되겠죠.
그렇기 때문에 저는 Python 런타임을 같이 사용할 수 있도록 정리해놓은 패키지 프레임워크가 있을 것이라고 생각하고, 검색 후 다음 깃허브를 찾을 수 있었습니다.
https://github.com/beeware/Python-Apple-support
GitHub - beeware/Python-Apple-support: A meta-package for building a version of Python that can be embedded into a macOS, iOS, t
A meta-package for building a version of Python that can be embedded into a macOS, iOS, tvOS or watchOS project. - beeware/Python-Apple-support
github.com
이 라이브러리를 이용해 파이썬 런타임을 Python.xcframework로 내장시키고, PythonKit을 사용하면 일차적으로 Python을 MacOS 앱 내부에서 사용할 준비는 끝납니다.
여기까지 따라오는데 힘들진 않았을 것이라 예상합니다.
하지만 우리는 파이썬을 사용하고자 런타임을 내장 시킨게 아닙니다.
우리의 황금고블린인 파이썬의 수많은 라이브러리들을 내장시키기 위해 나아가고 있는 중이죠.
여기서 문제점이 발생합니다.
2. 파이썬 프레임워크에 라이브러리 내장시키기
그럼 어떻게 파이썬 모듈들을 프레임워크에 내장 시킬 수 있을까요?
여기서 많은 고민들을 하기 시작합니다.
하지만 결론은 제일 간단하게 빌드 시 실행하는 단계, build phase에 넣기로 결정했습니다.
먼저 python lib경로를 찾고, 만약 없다면 package 폴더를 생성합니다.
일단 저장소를 캐시하면 추후 패키지나 앱을 업데이트 하고 싶을 때, 원치 않는 곳에서 오류가 날 수 있습니다.
그렇기 때문에 최대한 보수적으로 빌드하기 위해서 저는 캐시를 사용하지 않는 방식으로 스크립트를 작성했습니다.
# git 저장소 캐시 삭제를 위한 단계 추가
rm -rf ~/.cache/pip/git
# 또는 전체 pip 캐시 삭제
rm -rf ~/.cache/pip
다음으로는 부트스트랩에 사용할 파이썬 버전을 고정하고, 이것을 사용하도록 설정해야 합니다.
여기서 Mac OS 앱을 개발하기 위해서는 당연히 Mac을 사용할 것이고, 대부분은 Homebrew를 사용하실 것이기 때문에 Homebrew로 설치한 파이썬을 기준으로 스크립트를 작성하였습니다.
export PYTHONNOUSERSITE=1 를 사용해 사용자 전용 site-packages를 로드하지 않도록 격리시키고,
pip의 버전 체크를 비활성화 한 후 캐시를 제거해주었습니다.
# 1) 부트스트랩에 사용할 Python 고정: (A) Homebrew python@3.13 우선, (B) PATH의 python3.13
PY_VER="3.13"
APP_FW="$TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/Frameworks/Python.framework"
SITE_PKGS="$APP_FW/Versions/${PY_VER}/lib/python${PY_VER}/site-packages"
mkdir -p "$SITE_PKGS"
# (A) Homebrew python@3.13 (SSL 포함)
if [ -x /opt/homebrew/opt/python@3.13/bin/python3.13 ]; then
HOST_PY=/opt/homebrew/opt/python@3.13/bin/python3.13
elif command -v python3.13 >/dev/null 2>&1; then
# (B) PATH 어딘가의 python3.13
HOST_PY="$(command -v python3.13)"
else
echo "❌ Python 3.13 해석기를 찾을 수 없습니다. brew install python@3.13 로 설치하세요." >&2
exit 1
fi
echo "➡️ 부트스트랩에 사용할 Python: $("$HOST_PY" --version 2>&1)"
PYBIN="$HOST_PY"
# 2) site-packages 경로
export PYTHONNOUSERSITE=1
export PIP_DISABLE_PIP_VERSION_CHECK=1
export PIP_NO_CACHE_DIR=1
이제 이걸 완료했다면, 우리가 원하는 패키지를 선택하고 설치할 준비가 되었습니다.
간단하게 pip으로 패키지를 설치하면 됩니다.
"$PYBIN" -m pip install \
--upgrade --no-cache-dir --force-reinstall \
패키지 name \
--target "$SITE_PKGS"
옵션별 의미
- pip install
- → Python 패키지를 설치하는 기본 명령어.
- --upgrade
- → 이미 설치된 패키지가 있더라도, 더 최신 버전이 있으면 업그레이드함.
- --no-cache-dir(캐시에 남아있는 오래된 wheel 파일이나 tar.gz 파일을 쓰지 않도록 강제)
- → pip 캐시 디렉토리를 사용하지 않고, 항상 새로 다운로드해서 설치함.
- --force-reinstall(단순 업그레이드가 아니라 같은 버전이라도 덮어씀)
- → 해당 패키지가 이미 설치되어 있어도 무조건 다시 설치함.
아래는 제가 세팅한 pip bootstrap phase 전문입니다.
#!/bin/sh
set -e
# git 저장소 캐시 삭제를 위한 단계 추가
rm -rf ~/.cache/pip/git
# 또는 전체 pip 캐시 삭제
rm -rf ~/.cache/pip
# 1) 부트스트랩에 사용할 Python 고정: (A) Homebrew python@3.13 우선, (B) PATH의 python3.13
PY_VER="3.13"
APP_FW="$TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/Frameworks/Python.framework"
SITE_PKGS="$APP_FW/Versions/${PY_VER}/lib/python${PY_VER}/site-packages"
mkdir -p "$SITE_PKGS"
# (A) Homebrew python@3.13 (SSL 포함)
if [ -x /opt/homebrew/opt/python@3.13/bin/python3.13 ]; then
HOST_PY=/opt/homebrew/opt/python@3.13/bin/python3.13
elif command -v python3.13 >/dev/null 2>&1; then
# (B) PATH 어딘가의 python3.13
HOST_PY="$(command -v python3.13)"
else
echo "❌ Python 3.13 해석기를 찾을 수 없습니다. brew install python@3.13 로 설치하세요." >&2
exit 1
fi
echo "➡️ 부트스트랩에 사용할 Python: $("$HOST_PY" --version 2>&1)"
PYBIN="$HOST_PY"
# 2) site-packages 경로
export PYTHONNOUSERSITE=1
export PIP_DISABLE_PIP_VERSION_CHECK=1
export PIP_NO_CACHE_DIR=1
# 3) 프로젝트 패키지 설치 (다른 의존성 포함)
"$PYBIN" -m pip --version || { echo "❌ pip not found"; exit 1; }
"$PYBIN" -m pip install \
--upgrade --no-cache-dir --force-reinstall --no-deps \
https://github.com/qwertyquerty/pypresence/archive/master.zip \
--target "$SITE_PKGS"
"$PYBIN" -m pip install \
--upgrade --no-cache-dir --force-reinstall \
pyatv aiohttp \
--target "$SITE_PKGS"
이 phase를 완료하면 python 패키지를 framework에 집어 넣을 수 있습니다.
다만 실행시키면 오류가 뜰 것입니다.
이것을 해결하기 위해 Xcode 16기준, User Script Sandboxing를 꺼주어야만 우리가 만든 shell script가 파일에 접근할 수 있게 되면서 빌드가 성공하게 됩니다.
2.1 그 외 특수 케이스들
하지만 여기서 쉽게 끝났다면 제가 이 글을 쓰고 있지 않았을 것입니다.
우리가 쓰는 파이썬 패키지들 가운데는 순수 파이썬만으로 동작하는 것도 있지만, C 확장 모듈(.so) 을 포함하거나 TLS(HTTPS 보안 통신) 을 사용하는 경우가 많습니다.
3.1 OpenSSL 의존성으로 인한 로딩 실패
- Python의 네이티브 확장(.so)이 SSL/TLS 기능을 사용할 경우, OpenSSL 라이브러리에 대한 의존성이 생기는데 macOS 앱 번들 내부에 OpenSSL 라이브러리를 내장시키지 않으면 dyld: Library not loaded 오류가 발생합니다.
Vendor SSL 스크립트는 Homebrew에서 설치된 libssl.3.dylib와 libcrypto.3.dylib을 Frameworks/OpenSSL로 복사해줍니다.
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 Vendor OpenSSL (libcrypto/libssl) → Frameworks/OpenSSL"
mkdir -p "$DEST_OPENSSL_DIR"
if [ -f "$SRC_CRYPTO" ] && [ -f "$SRC_SSL" ]; then
"$RSYNC" -a "$SRC_CRYPTO" "$DEST_OPENSSL_DIR/"
"$RSYNC" -a "$SRC_SSL" "$DEST_OPENSSL_DIR/"
# Set self IDs to @rpath/OpenSSL/* so dependent binaries can reference via rpath
safe_install_name_tool -id "@rpath/OpenSSL/libcrypto.3.dylib" "$DEST_OPENSSL_DIR/libcrypto.3.dylib"
safe_install_name_tool -id "@rpath/OpenSSL/libssl.3.dylib" "$DEST_OPENSSL_DIR/libssl.3.dylib"
# Ensure libssl references libcrypto via @rpath, too
patch_openssl_refs "$DEST_OPENSSL_DIR/libssl.3.dylib"
else
echo "⚠️ OpenSSL not found at $OPENSSL_SRC_PREFIX — skipping copy."
echo " (If you vendor OpenSSL differently, ensure files exist in $DEST_OPENSSL_DIR.)"
fi
.so 및 관련 바이너리가 올바르게 @rpath/OpenSSL/...를 참조하도록 내부 의존경로를 수정해주는 스크립트입니다.
patch_openssl_refs() {
bin="$1"
FROM_CRYPTO="$OPENSSL_SRC_PREFIX/lib/libcrypto.3.dylib"
FROM_SSL="$OPENSSL_SRC_PREFIX/lib/libssl.3.dylib"
TO_CRYPTO="@rpath/OpenSSL/libcrypto.3.dylib"
TO_SSL="@rpath/OpenSSL/libssl.3.dylib"
if "$OTOOL" -L "$bin" | grep -q "$FROM_CRYPTO"; then
echo " • patch libcrypto -> @rpath ($bin)"
safe_install_name_tool -change "$FROM_CRYPTO" "$TO_CRYPTO" "$bin"
fi
if "$OTOOL" -L "$bin" | grep -q "$FROM_SSL"; then
echo " • patch libssl -> @rpath ($bin)"
safe_install_name_tool -change "$FROM_SSL" "$TO_SSL" "$bin"
fi
}
메인 파일 및 네이티브 확장에서 로드 할 때 필요한 @rpath경로(@excutable_path/../Frameworks/OpenSSL)을 RPATH에 추가해주는 스크립트입니다.
add_rpath_if_missing() {
bin="$1"; rpath="$2"
if "$OTOOL" -l "$bin" | grep -A2 LC_RPATH | grep -q "$rpath"; then
return 0
fi
echo " • add_rpath $rpath -> $(basename "$bin")"
if ! safe_install_name_tool -add_rpath "$rpath" "$bin"; then
echo " ⚠️ add_rpath failed (likely headerpad too small)."
echo " ➜ Prefer link‑time LD_RUNPATH_SEARCH_PATHS to include:"
echo " @executable_path/../Frameworks"
echo " @executable_path/../Frameworks/OpenSSL"
fi
}
- (주의점) Runpath Search Path에 @executable_path/../Frameworks/OpenSSL을 추가해주셔야 install name tool의 바이너리 로드 커맨드 공간(headerpad)가 부족함 없이 동작하기 때문에 넣어주셔야 합니다.
3.2 .so(파이썬 네이티브 확장) 재서명 필요
- code signature not valid for use in process, mapping process and mapped file have different Team IDs 오류가 뜨는 경우가 있습니다.
- macOS의 Hardened Runtime 때문에 앱이 로드하는 모든 네이티브 코드(.dylib, .so) 가 동일 Team ID로 서명되어 있어야 하기 때문으로, 아래 스크립트들을 통해 해결해주었습니다.
sign_file - 단일 파일 서명
sign_file() {
"$CODESIGN" --force --options runtime --timestamp --sign "$SIGN_IDENTITY" "$1"
}
이 함수는 지정한 파일을 Hardened Runtime 옵션으로 지정한 ID로 서명하도록 해줍니다.
런타임에 로드되는 네이티브 코드를 OS가 신뢰하도록 만들어주는 역할입니다.
sign_glob_under — 폴더 내 다수 파일 일괄 서명
sign_glob_under() {
dir="$1"; pat="$2"
[ -d "$dir" ] || return 0
"$FIND" "$dir" -type f -name "$pat" -print0 | while IFS= read -r -d '' f; do
echo " • codesign $f"
sign_file "$f"
done
}
이 함수는 find로 dir 아래 pat에 매칭되는 모든 파일을 찾아 일괄 서명하는 역할인데,
파이썬 확장들은 보통 여러개에다 경로도 다양하기 때문에, 누락 없이 한번에 처리하기 위해 사용했습니다.
(참고) sign_file_maybe_entitlements
이 함수의 경우 Entitlements 파일이 지정된 경우에만 그 entitlements를 붙여 서명하는 역할인데, 보통 .so/.dylib에 별도 entitlements는 필요없으므로 .so 서명에는 sign_file을 사용합니다.
# Python natives & framework
if [ "$PY_HAS_FRAMEWORK" -eq 1 ]; then
sign_glob_under "$PY_LIBDIR/lib-dynload" "*.so"
sign_glob_under "$PY_LIBDIR/site-packages" "*.so"
sign_glob_under "$PY_LIBDIR/site-packages" "*.dylib"
sign_glob_under "$FW_BASE/lib" "*.dylib"
sign_file "$PY_FW_DIR"
fi
위에 언급한 함수들을 이용해 이렇게, 서명하면 됩니다.
3.3 Xcode가 생성하는 보조 바이너리로 인한 오류
- 이는 보조적인 오류인데요, 원래 발생하지 않았다가 Xcode 16부터 생긴 이슈인 것 같아 기록차원에서 남겨놓습니다.
- 메인 바이너리 서명 할 때, “In subcomponent: …/MacOS/__preview.dylib” 또는 …/PRODUCT_NAME.debug.dylib로 인해
“code object is not signed at all” 등의 에러가 발생할 수 있습니다.
이는 SwiftUI Preview용 __preview.dylib, 디버그 보조 산출물 *.debug.dylib 이 앱 번들에 들어와 서명 트리를 깨뜨리는게 원인인데요, Build Option에서 Enable Debug Dylib Support를 NO로 바꿔주시면 됩니다.
다음은 다음 오류를 전부 수정할 수 있는 shell script 전문입니다.
#!/bin/sh
##############################################################################
# 0) Common paths & inputs
##############################################################################
APP_WRAPPER="$TARGET_BUILD_DIR/$WRAPPER_NAME"
APP_MACOS="$APP_WRAPPER/Contents/MacOS"
APP_RES="$APP_WRAPPER/Contents/Resources"
APP_FWKS="$APP_WRAPPER/Contents/Frameworks"
# Configurable: Apple Silicon Homebrew prefix by default
OPENSSL_SRC_PREFIX="${OPENSSL_SRC_PREFIX:-/opt/homebrew/opt/openssl@3}"
SRC_CRYPTO="$OPENSSL_SRC_PREFIX/lib/libcrypto.3.dylib"
SRC_SSL="$OPENSSL_SRC_PREFIX/lib/libssl.3.dylib"
DEST_OPENSSL_DIR="$APP_FWKS/OpenSSL"
# Optional extras
ENTITLEMENTS_FILE="${ENTITLEMENTS_FILE:-}"
EXTRA_SIGN_BINARIES="${EXTRA_SIGN_BINARIES:-}"
PYTHON_FRAMEWORK_NAME="Python.framework"
PY_FW_DIR="$APP_FWKS/$PYTHON_FRAMEWORK_NAME"
PY_HAS_FRAMEWORK=0
# Tools (absolute paths to avoid PATH shadowing)
CODESIGN="/usr/bin/codesign"
OTOOL="/usr/bin/otool"
INTOOL="/usr/bin/install_name_tool"
CHMOD="/bin/chmod"
FIND="/usr/bin/find"
RSYNC="/usr/bin/rsync"
SIGN_IDENTITY="${EXPANDED_CODE_SIGN_IDENTITY:-}"
if [ -z "$SIGN_IDENTITY" ]; then
echo "❌ EXPANDED_CODE_SIGN_IDENTITY is empty — configure Signing." >&2
exit 1
fi
##############################################################################
# 1) Helpers
##############################################################################
sign_file() {
"$CODESIGN" --force --options runtime --timestamp --sign "$SIGN_IDENTITY" "$1"
}
sign_file_maybe_entitlements() {
file="$1"
if [ -n "$ENTITLEMENTS_FILE" ] && [ -f "$ENTITLEMENTS_FILE" ]; then
"$CODESIGN" --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS_FILE" \
--sign "$SIGN_IDENTITY" "$file"
else
sign_file "$file"
fi
}
XATTR="/usr/bin/xattr"
sign_file_nonfatal() {
file="$1"
# Best effort: clear quarantine and make writable, then try codesign; never fail the build
$XATTR -dr com.apple.quarantine "$file" 2>/dev/null || true
/bin/chmod u+w "$file" 2>/dev/null || true
if [ -n "$ENTITLEMENTS_FILE" ] && [ -f "$ENTITLEMENTS_FILE" ]; then
"$CODESIGN" --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS_FILE" \
--sign "$SIGN_IDENTITY" "$file" || echo " ⚠️ nonfatal: codesign failed for $file"
else
"$CODESIGN" --force --options runtime --timestamp \
--sign "$SIGN_IDENTITY" "$file" || echo " ⚠️ nonfatal: codesign failed for $file"
fi
}
sign_glob_under() {
dir="$1"; pat="$2"
[ -d "$dir" ] || return 0
# NUL‑delimited traversal to survive spaces in paths
"$FIND" "$dir" -type f -name "$pat" -print0 | while IFS= read -r -d '' f; do
echo " • codesign $f"
sign_file "$f"
done
}
safe_install_name_tool() {
# Usage: safe_install_name_tool <args...> <target>
# Make target writable and do not fail the whole build if a change is impossible.
# Determine last arg as target
set -- "$@"; last="$*"; target=${last##* }
"$CHMOD" u+w "$target" 2>/dev/null || true
if ! "$INTOOL" "$@"; then
echo " ⚠️ install_name_tool failed on $(basename "$target") — continuing; will re‑sign later"
return 0
fi
}
add_rpath_if_missing() {
bin="$1"; rpath="$2"
if "$OTOOL" -l "$bin" | grep -A2 LC_RPATH | grep -q "$rpath"; then
return 0
fi
echo " • add_rpath $rpath -> $(basename "$bin")"
if ! safe_install_name_tool -add_rpath "$rpath" "$bin"; then
echo " ⚠️ add_rpath failed (likely headerpad too small)."
echo " ➜ Prefer link‑time LD_RUNPATH_SEARCH_PATHS to include:"
echo " @executable_path/../Frameworks"
echo " @executable_path/../Frameworks/OpenSSL"
fi
}
patch_openssl_refs() {
bin="$1"
FROM_CRYPTO="$OPENSSL_SRC_PREFIX/lib/libcrypto.3.dylib"
FROM_SSL="$OPENSSL_SRC_PREFIX/lib/libssl.3.dylib"
TO_CRYPTO="@rpath/OpenSSL/libcrypto.3.dylib"
TO_SSL="@rpath/OpenSSL/libssl.3.dylib"
if "$OTOOL" -L "$bin" | grep -q "$FROM_CRYPTO"; then
echo " • patch libcrypto -> @rpath ($bin)"
safe_install_name_tool -change "$FROM_CRYPTO" "$TO_CRYPTO" "$bin"
fi
if "$OTOOL" -L "$bin" | grep -q "$FROM_SSL"; then
echo " • patch libssl -> @rpath ($bin)"
safe_install_name_tool -change "$FROM_SSL" "$TO_SSL" "$bin"
fi
}
##############################################################################
# 2) Vendor OpenSSL into app & set install_name IDs
##############################################################################
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 Vendor OpenSSL (libcrypto/libssl) → Frameworks/OpenSSL"
mkdir -p "$DEST_OPENSSL_DIR"
if [ -f "$SRC_CRYPTO" ] && [ -f "$SRC_SSL" ]; then
"$RSYNC" -a "$SRC_CRYPTO" "$DEST_OPENSSL_DIR/"
"$RSYNC" -a "$SRC_SSL" "$DEST_OPENSSL_DIR/"
# Set self IDs to @rpath/OpenSSL/* so dependent binaries can reference via rpath
safe_install_name_tool -id "@rpath/OpenSSL/libcrypto.3.dylib" "$DEST_OPENSSL_DIR/libcrypto.3.dylib"
safe_install_name_tool -id "@rpath/OpenSSL/libssl.3.dylib" "$DEST_OPENSSL_DIR/libssl.3.dylib"
# Ensure libssl references libcrypto via @rpath, too
patch_openssl_refs "$DEST_OPENSSL_DIR/libssl.3.dylib"
else
echo "⚠️ OpenSSL not found at $OPENSSL_SRC_PREFIX — skipping copy."
echo " (If you vendor OpenSSL differently, ensure files exist in $DEST_OPENSSL_DIR.)"
fi
##############################################################################
# 3) Patch all MacOS/* for RPATH + deps (executables & dylibs)
##############################################################################
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔁 Fix RPATH & deps in Contents/MacOS/*"
if [ -d "$APP_MACOS" ]; then
"$FIND" "$APP_MACOS" -type f \( -name "*.dylib" -o -perm -111 \) -print0 \
| while IFS= read -r -d '' bin; do
add_rpath_if_missing "$bin" "@executable_path/../Frameworks"
add_rpath_if_missing "$bin" "@executable_path/../Frameworks/OpenSSL"
patch_openssl_refs "$bin"
done
fi
# Optional: extra specific binaries to patch/sign (newline or space separated)
if [ -n "$EXTRA_SIGN_BINARIES" ]; then
echo "🔁 Patch deps in EXTRA_SIGN_BINARIES"
printf '%s\n' $EXTRA_SIGN_BINARIES | while IFS= read -r D; do
[ -n "$D" ] && [ -e "$D" ] && patch_openssl_refs "$D" || true
done
fi
##############################################################################
# 4) Python.framework detection & patching of native modules
##############################################################################
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔁 Patch deps in Python natives (.so/.dylib)"
if [ -d "$PY_FW_DIR" ]; then
PY_HAS_FRAMEWORK=1
if [ -L "$PY_FW_DIR/Versions/Current" ]; then
PY_VER_DIR=`readlink "$PY_FW_DIR/Versions/Current"`
else
# Pick highest 3.x directory in Versions
PY_VER_DIR=`ls -d "$PY_FW_DIR"/Versions/3.* 2>/dev/null \
| sed 's#.*/##' \
| awk -F. '{print $1"."$2}' \
| sort -t. -k2,2n \
| tail -n1`
fi
if [ -z "$PY_VER_DIR" ]; then
echo "❌ Unable to determine Python.framework version" >&2
exit 1
fi
PY_VER="$PY_VER_DIR" # e.g. 3.13
FW_BASE="$PY_FW_DIR/Versions/$PY_VER"
PY_LIBDIR="$FW_BASE/lib/python$PY_VER" # e.g. .../lib/python3.13
echo "🐍 Python.framework: $PY_VER"
for scan_dir in \
"$PY_LIBDIR/lib-dynload" \
"$PY_LIBDIR/site-packages" \
"$FW_BASE/lib"
do
[ -d "$scan_dir" ] || continue
"$FIND" "$scan_dir" -type f \( -name "*.so" -o -name "*.dylib" \) -print0 \
| while IFS= read -r -d '' f; do
patch_openssl_refs "$f"
done
done
else
echo "ℹ️ Python.framework not found — skipping Python patch/sign."
fi
# Resources/PythonSupport mirror (if you vendor Python there too)
RES_PYSUP="$APP_RES/PythonSupport"
if [ -d "$RES_PYSUP" ]; then
"$FIND" "$RES_PYSUP" -type f \( -name "*.so" -o -name "*.dylib" -o -perm -111 \) -print0 \
| while IFS= read -r -d '' f; do
patch_openssl_refs "$f"
done
fi
##############################################################################
# 5) Codesign: OpenSSL → Python natives → Framework → Resources → App
##############################################################################
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔐 Codesign everything"
# OpenSSL
sign_glob_under "$DEST_OPENSSL_DIR" "*.dylib"
# Python natives & framework
if [ "$PY_HAS_FRAMEWORK" -eq 1 ]; then
sign_glob_under "$PY_LIBDIR/lib-dynload" "*.so"
sign_glob_under "$PY_LIBDIR/site-packages" "*.so"
sign_glob_under "$PY_LIBDIR/site-packages" "*.dylib"
sign_glob_under "$FW_BASE/lib" "*.dylib"
sign_file "$PY_FW_DIR"
fi
# Resources/PythonSupport
if [ -d "$RES_PYSUP" ]; then
sign_glob_under "$RES_PYSUP" "*.so"
sign_glob_under "$RES_PYSUP" "*.dylib"
fi
# All resource executables (if any); use ENTITLEMENTS_FILE when provided
if [ -d "$APP_RES" ]; then
"$FIND" "$APP_RES" -type f -perm -111 -print0 \
| while IFS= read -r -d '' exe; do
echo " • codesign resource exec $exe"
sign_file_maybe_entitlements "$exe"
done
fi
# Extra explicit binaries to sign (if provided)
if [ -n "$EXTRA_SIGN_BINARIES" ]; then
printf '%s\n' $EXTRA_SIGN_BINARIES | while IFS= read -r D; do
[ -n "$D" ] && [ -e "$D" ] && sign_file "$D" || true
done
fi
# Main binary (re‑sign last)
if [ -f "$APP_MACOS/$PRODUCT_NAME" ]; then
sign_file "$APP_MACOS/$PRODUCT_NAME"
fi
echo "✅ Done: OpenSSL vendoring + @rpath patch + codesign"
단순히 복사/붙여넣기만 하셔도 대부분의 상황에서는 아마 동작할 듯 싶어서 공유드립니다.
여기까지 잘 따라 오셨다면 이제 앱에 아름답게 빌드 된 우리의 라이브러리들을 확인하실 수 있습니다.
다음은 이렇게 빌드된 파이썬을 어떻게 Applepie-RPC앱에서 메인스레드의 제약에서 벗어나 서비스 레이어에서 사용할 수 있었는지에 대해 다뤄보도록 하겠습니다.
또한 시간이 된다면 iOS앱에서 사용하는 방법도 테스트 검증 및 정리해보도록 하겠습니다.
긴 글 읽어주셔서 감사합니다.
덧붙이거나 더 이야기하고 싶은 내용은 자유롭게 댓글 달아주시면 확인 후 답변 드리겠습니다. :)