This project provides a set of shell scripts to build a relocatable (portable) Python.framework
for macOS. It downloads Python from Beeware's Python-Apple-support repository, configures it, builds it, slims it down, patches its internal library paths (RPATHs) for portability, compiles bytecode, and signs the entire framework. Optionally, it can include a site-packages
directory populated from a requirements.txt
file, also with patched binaries.
The goal is to create a Python framework that can be bundled within a macOS application and function correctly without relying on system Python installations or specific install paths.
- Downloads and builds a specific Python version (default: 3.13).
- Supports
arm64
andx86_64
architectures (builds for one chosen architecture). - Modifies Python's
release.macOS.exclude
to includebin
andPython.app
. - Extensively patches dylib IDs and RPATHs of the Python framework and bundled libraries to use
@loader_path
and@rpath
for true portability. - Slims the framework by removing unnecessary files and directories (tests, docs, etc.).
- Performs architecture-specific slimming (lipo) if not building a universal binary.
- Compiles all
.py
files to.pyc
bytecode. - Optionally creates a virtual environment using the built framework, installs packages from
requirements.txt
, and bundles them into the framework'ssite-packages
.- Binaries within these site-packages are also RPATH-patched, slimmed, and correctly signed.
- Signs the entire framework and its contents in the correct order using a specified developer identity, handling entitlements appropriately for executables versus libraries.
- Includes smoke tests to verify the final framework's integrity and basic functionality.
- Comprehensive logging for easier debugging.
- macOS: These scripts are designed for macOS.
- Xcode Command Line Tools: Essential for
git
,make
,lipo
,codesign
,otool
,install_name_tool
, etc.xcode-select --install
- Homebrew (Recommended): For easily installing an external Python version.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- External Python: A Python interpreter matching the major.minor version of
PYTHON_VERSION_TAG
(see Configuration). This is required for bytecode compilation (compileall
). It's best if this is a separate installation (e.g., via Homebrew) and not the system Python.# Example for Python 3.13 brew install python@3.13
- Apple Developer ID Certificate: For code signing. You need a valid signing identity installed in your keychain.
requirements.txt
(Optional): IfINCLUDE_SITE_PACKAGES
is true, this file should be in the project root, listing packages to install.debug.entitlements
(Provided): A basic entitlements file allowing debugging is included.
Modify these variables at the top of the main build_framework.sh
script:
PYTHON_VERSION_TAG
: The tag/branch from Beeware's Python-Apple-support to build (e.g., "3.13", "3.11").- Default: "3.13"
SIGNING_IDENTITY_FILENAME
: Name of the file (relative to project root) that will store the chosen signing identity HASH.- Default: ".signing_identity"
ARCHITECTURE
: Target architecture. Can be "arm64" or "x86_64". Due to RPATH limitations, universal binaries are not supported by this script.- Default: "arm64"
INCLUDE_SITE_PACKAGES
: Set totrue
orfalse
. If true, packages fromrequirements.txt
will be installed into the framework.- Default: true
- Clone the repository (or set up your project with these scripts).
- Ensure all prerequisites are met.
- Configure
build_framework.sh
as described above. - Create
requirements.txt
in the project root ifINCLUDE_SITE_PACKAGES=true
. - Make the main script executable:
chmod +x build_framework.sh
- Run the main build script:
The script will prompt you to select a signing identity on the first run (or if the saved one is invalid). Build logs will be saved to the
./build_framework.sh
logs/
directory.
The build_framework.sh
script orchestrates a series of sub-scripts located in the build_framework_scripts/
directory. Each step is logged to the console and to a detailed log file in the logs/
directory.
Step 00: Helpers & Initial Setup (00_helpers.sh
)
- Explanation: Sets up shared helper functions (logging, environment setup, tool checks) and global settings.
- Technical Details:
- Defines ANSI color codes for logging.
- Provides
log_info
,log_success
,log_warning
,log_error
functions. setup_build_environment
: ValidatesPYTHON_VERSION_TAG
andARCHITECTURE
, createsbuild/
andtemp/
directories.check_command_line_tools
: Verifies presence ofbrew
,git
,make
, etc., and finds a compatible external Python for bytecode compilation.liposuction_binary_file
: Helper to slim Mach-O universal binaries to a single architecture usinglipo -thin
.
Step 01: Get Python Source (01_get_python.sh
)
- Explanation: Downloads or updates the Python source code from Beeware's Python-Apple-support repository for the specified version.
- Technical Details:
- Clones
https://github.com/beeware/Python-Apple-support
intotemp/Python-Apple-support
. - If already cloned, fetches latest changes and checks out the tag/branch specified by
PYTHON_VERSION_TAG
.
- Clones
Step 02: Configure Python Build (02_configure_python.sh
)
- Explanation: Modifies the build configuration for the Python source to ensure necessary components like
bin
(executables) andPython.app
are included in the framework. - Technical Details:
- Uses
sed
to comment out lines intemp/Python-Apple-support/patch/Python/release.macOS.exclude
that would otherwise excludeVersions/*/bin
andResources/Python.app
.
- Uses
Step 03: Build Python.framework (03_build_python_framework.sh
)
- Explanation: Compiles the Python source code to produce the initial
Python.framework
. - Technical Details:
- Navigates to
temp/Python-Apple-support
. - Runs
make clean
andmake macOS
. - Extracts the
Python.framework
from the resulting tarball (dist/Python-${PYTHON_VERSION_TAG}-macOS-support.custom.tar.gz
) into thebuild/
directory. - Cleans up the
temp/Python-Apple-support
source directory.
- Navigates to
Step 04: Determine Framework Python Version (04_determine_framework_python_version.sh
)
- Explanation: Inspects the built
Python.framework
to determine the exact Python version (e.g., 3.13.1) and major.minor version (e.g., 3.13). This is needed for subsequent path constructions. - Technical Details:
- Reads the symlink
build/Python.framework/Versions/Current
to find the actual version string. - Saves
ACTUAL_PYTHON_VERSION
andPYTHON_VERSION_MAJOR_MINOR
tobuild/.python_versions_info
for use by later scripts.
- Reads the symlink
Step 05: Slim Python Framework (05_slim_python.sh
)
- Explanation: Removes unnecessary components from the
Python.framework
to reduce its size. - Technical Details:
- Removes directories like
lib/pkgconfig
, and standard library modules likeidlelib
,tkinter
,test
. - Removes specific executables from
bin/
(e.g.,idle3
,pydoc3
,python3-config
). - Deletes pre-existing
*.pyc
,*.pyo
files and__pycache__
directories (they will be regenerated later). - If
ARCHITECTURE
is not "universal", usesliposuction_binary_file
(andlipo
) to slim all Mach-O binaries within the framework to the target architecture.
- Removes directories like
Step 06: Patch Framework Binaries (06_patch_framework_binaries.sh
)
- Explanation: This crucial step modifies the internal library linkage paths (RPATHs) of the framework's executables and dynamic libraries to make them portable.
- Technical Details: Uses
otool -L
to inspect dependencies andinstall_name_tool
to:- Change absolute dependency paths in
bin/pythonX.Y
andResources/Python.app/Contents/MacOS/Python
to relative@loader_path/...
references. - Set the
id
of the mainPython
dylib to the standard@rpath/...
format. - For other bundled dylibs (e.g.,
libssl.dylib
,libcrypto.dylib
):- Sets their
id
to@rpath/<dylib_basename>
. - Adds
@loader_path/.
as an RPATH (to find sibling dylibs). - Changes their dependencies on other bundled dylibs to use
@rpath/...
.
- Sets their
- For
.so
extension modules inlib-dynload/
:- Cleans up build-time RPATHs.
- Adds RPATHs (
@loader_path/../../
and@loader_path/../../../
) to point to the framework'slib/
andVersions/X.Y.Z/
directories. - Changes their dependency on the main Python library to
@rpath/Python
. - Changes dependencies on bundled dylibs to
@rpath/...
.
- Change absolute dependency paths in
Step 07: Compile Python Bytecode (07_compile_python_bytecode.sh
)
- Explanation: Compiles all Python source files (
.py
) within the framework's standard library to bytecode files (.pyc
). - Technical Details:
- Uses the
EXTERNAL_PYTHON_EXEC
(found in Step 00) to runpython -m compileall -f -q
on the framework'slib/pythonX.Y
directory.
- Uses the
Step 08: Initial Sign Python Framework (08_initial_sign_python.sh
)
- Explanation: Performs an initial code signing of all components within the
Python.framework
. This step allows the framework's Python executable to be used for creating a virtual environment in the next step. - Technical Details:
- Prompts the user to select a code signing identity (HASH) if not already saved in
SIGNING_IDENTITY_FILENAME
. - Signs files in a specific order (deepest first):
.so
files inlib-dynload/
(no entitlements)..dylib
files inlib/
(no entitlements).- The main
Python
dylib (no entitlements). - Executables in
bin/
(withdebug.entitlements
allowingget-task-allow
). Python.app
bundle (withdebug.entitlements
).- The top-level
Python.framework
bundle (no entitlements).
- Uses
codesign --force --sign <identity> --options runtime --timestamp
.
- Prompts the user to select a code signing identity (HASH) if not already saved in
Step 09: Setup Virtual Environment and Site-Packages (09_setup_venv_and_sitepackages.sh
) (Optional)
- Explanation: If
INCLUDE_SITE_PACKAGES
is true, this step creates a temporary virtual environment using the just-built and signed framework's Python, then installs packages fromrequirements.txt
into it. - Technical Details:
- Uses
build/Python.framework/Versions/Current/bin/python3 -m venv temp/venv
. - Upgrades
pip
in the venv. - Installs packages using
temp/venv/bin/pip install -r requirements.txt
. - Moves the
site-packages
directory from the venv (temp/venv/lib/pythonX.Y/site-packages
) tobuild/PythonPackages
for further processing. - Cleans up
temp/venv
.
- Uses
Step 10: Patch Site-Packages Binaries (10_patch_sitepackages_binaries.sh
) (Optional)
- Explanation: If site-packages were included, this step patches the RPATHs of any Mach-O binaries (.so, .dylib) within
build/PythonPackages
to ensure they correctly link against the main Python framework and other libraries. - Technical Details: Similar to Step 06, but targets binaries in
build/PythonPackages
.- Fixes dylib IDs to
@rpath/<basename>
. - Adds
@loader_path/.
RPATH to dylibs within packages. - Cleans up existing RPATHs.
- Adds RPATHs pointing to the main framework's library locations (e.g.,
@loader_path/../../../
) and to local library directories within packages (e.g.,@loader_path/.dylibs
,@loader_path/../lib
). - Includes special RPATH handling for packages like SciPy and TorchVision.
- Changes dependencies to link against the main Python framework using its
@rpath/...
ID and other framework/package dylibs using@rpath/<basename>
.
- Fixes dylib IDs to
Step 11: Slim Site-Packages (11_slim_venv.sh
) (Optional)
- Explanation: If site-packages were included, this step removes unnecessary files and directories (tests, docs, metadata) from
build/PythonPackages
and performs architecture slimming on its binaries. - Technical Details:
- Removes common clutter like
tests/
,docs/
,*.egg-info
,*.dist-info
,__pycache__
frombuild/PythonPackages
. - If
ARCHITECTURE
is not "universal", usesliposuction_binary_file
to slim Mach-O binaries inbuild/PythonPackages
.
- Removes common clutter like
Step 12: Compile Site-Packages Bytecode (12_compile_venv_bytecode.sh
) (Optional)
- Explanation: If site-packages were included, this compiles all
.py
files inbuild/PythonPackages
to.pyc
bytecode. - Technical Details:
- Uses
EXTERNAL_PYTHON_EXEC -m compileall -f -q
onbuild/PythonPackages
.
- Uses
Step 13: Move Site-Packages to Framework (13_move_venv_to_framework.sh
) (Optional)
- Explanation: If site-packages were processed, this step moves the prepared
build/PythonPackages
directory into its final location within thePython.framework
. - Technical Details:
- Moves
build/PythonPackages
tobuild/Python.framework/Versions/${ACTUAL_PYTHON_VERSION}/lib/python${PYTHON_VERSION_MAJOR_MINOR}/site-packages
.
- Moves
Step 14: Final Sign Framework (14_final_sign_framework.sh
)
- Explanation: Performs a final, deep code sign on the entire
Python.framework
, now including any site-packages. This ensures all new or modified files are correctly signed. A crucial aspect of this step is pre-signing any binaries withinsite-packages
(if included) with appropriate runtime options before the final deep sign of the entire framework. - Technical Details:
- If
site-packages
are included and contain.so
or.dylib
files, these are first individually signed usingcodesign --force --sign <identity> --options runtime --timestamp
. This ensures they are signed with the hardened runtime but without specific executable entitlements. - Then, the entire
Python.framework
is signed usingcodesign --force --deep --sign <identity> --options runtime --timestamp --entitlements debug.entitlements Python.framework
. The--deep
option handles nested components, and the specified entitlements (e.g.,debug.entitlements
) are applied to the main executables within the framework (likePython.app/Contents/MacOS/Python
). - Verifies the final signature using
codesign --verify --deep --strict
.
- If
Step 15: Smoke Test Framework (15_smoke_test_framework.sh
)
- Explanation: Runs basic tests on the final, signed
Python.framework
to ensure it's functional. - Technical Details:
- Verifies the framework's code signature again.
- Executes a simple "Hello World" Python script using the framework's
Python.app
executable. - Tests importing a standard library module with C extensions (e.g.,
ssl
).
The primary output is a relocatable Python.framework
located in the build/
directory.
Detailed logs are stored in the logs/
directory.
- "ERROR: This script should not be run directly.": Ensure you are running the main
build_framework.sh
script, not the individual scripts inbuild_framework_scripts/
. - Missing Command Line Tools: Install Xcode Command Line Tools and any other missing tools reported by
00_helpers.sh
. - Signing Issues:
- Ensure you have a valid Apple Developer ID certificate in your keychain.
- If
security find-identity
shows no identities, you need to set one up. - Verify the
SIGNING_IDENTITY_FILENAME
(default:.signing_identity
) contains the correct HASH of your chosen identity. - Ensure your main application (if embedding this framework) is signed with the same Team ID as the framework.
ImportError: dlopen(...so, ...): ... not valid for use in process: mapping process and mapped file (non-platform) have different Team IDs
:- This is a common code signing error indicating a conflict between the Team ID or entitlements of your app's Python interpreter and the C extension module (
.so
file) it's trying to load (often fromsite-packages
). - The build scripts (specifically
14_final_sign_framework.sh
) attempt to mitigate this by:- Pre-signing individual
.so
/.dylib
files insite-packages
with the hardened runtime (--options runtime
) but without specific executable entitlements. - Performing a
--deep
sign on the entirePython.framework
, applying executable-specific entitlements (likedebug.entitlements
) only to the framework's main executables.
- Pre-signing individual
- Ensure your Xcode project is embedding and signing the
Python.framework
correctly and that the Team ID used for signing your main app matches the one used for the framework. - Manually verify signatures and entitlements using
codesign -dvvv --entitlements - /path/to/binary
for your app, the Python executable inside the framework, and the problematic.so
file. AllTeamIdentifier
fields must match.
- This is a common code signing error indicating a conflict between the Team ID or entitlements of your app's Python interpreter and the C extension module (
- Python Version Mismatch for Bytecode Compilation: Ensure the external Python version (e.g., from Homebrew) matches the
PYTHON_VERSION_TAG
major.minor version. - RPATH/Linking Errors in Bundled App: If your app using this framework fails with library loading errors (
dyld: Library not loaded: ...
), useotool -L <your_app_executable>
andotool -L <path_to_dylib_in_framework>
to investigate. The RPATH patching steps (06 and 10) are critical. Ensure your app's Runpath Search Paths (LD_RUNPATH_SEARCH_PATHS
in Xcode) correctly point to the location of the embeddedPython.framework
(e.g.,@executable_path/../Frameworks
). - Check Logs: The
logs/build_YYYYMMDD_HHMMSS.log
file contains detailed output from all steps.
Contributions are welcome! Please feel free to submit pull requests or open issues for bugs, feature requests, or improvements.
MIT, please share, improve, help out :D