Files
UnrealEngine/Engine/Source/ThirdParty/Python3/CopyPythonInstall_Mac.command
2025-05-18 13:04:45 +08:00

353 lines
16 KiB
Bash

#!/bin/sh
# --------------------------------------------------------------------------------
# Introduction
# --------------------------------------------------------------------------------
# To distribute Python with UE5 on Mac, it must built locally and the produced binaries install names must
# be updated to support relocation (so that the libraries are found whereever the user installs UE. We also
# need to build universal binaries (Intel x64 and ARM M1) and support various version of MacOS. This script
# is meant to help with that. Note that the binaries you downloaod from Python.org are not relocatable and the
# the binaries are signed, so we cannot fix that with install_name_tool because it breaks the signature.
# We really need to build that stuff.
#
# --------------------------------------------------------------------------------
# Steps
# --------------------------------------------------------------------------------
# - Download the desired Python version from https://www.python.org/ftp/python/
# - Download xz (liblzma) from https://tukaani.org/xz/
# - Unpack the downloaded packages somewhere. ex: $VolumeRoot/Build/
# - Copy OpenSSL from Engine/Source/ThirdParty/OpenSSL into $VolumeRoot/Deploy
# - Copy zlib from Engine/Source/ThirdParty/zlib into $VolumeRoot/Deploy
# - Build lzma for x64 architecture.
# cd $VolumeRoot/Build/xz-5.4.6
# ./configure --prefix=$VolumeRoot/Deploy/xz-5.4.6 --disable-xz --disable-lzma-links --disable-scripts --disable-doc CFLAGS="-isysroot `xcrun --sdk macosx --show-sdk-path` -mmacosx-version-min=11.0 -gdwarf-2 -arch x86_64" CPPFLAGS="-mmacosx-version-min=11.0 -gdwarf-2 -arch x86_64" LDFLAGS="-mmacosx-version-min=11.0 -arch x86_64"
# make -j28
# make install
# mv $VolumeRoot/Deploy/xz-5.4.6/lib $VolumeRoot/Deploy/xz-5.4.6/lib_x64
# - Build lzma for ARM architecture
# make clean
# ./configure --prefix=$VolumeRoot/Deploy/xz-5.4.6 --host=aarch64-apple-darwin --disable-xz --disable-lzma-links --disable-scripts --disable-doc CFLAGS="-isysroot `xcrun --sdk macosx --show-sdk-path` -mmacosx-version-min=11.0 -gdwarf-2 -arch arm64" CPPFLAGS="-mmacosx-version-min=11.0 -gdwarf-2 -arch arm64" LDFLAGS="-mmacosx-version-min=11.0 -arch arm64"
# make -j28
# make install
# mv $VolumeRoot/Deploy/xz-5.4.6/lib $VolumeRoot/Deploy/xz-5.4.6/lib_arm
# - Smash lzma x64 and ARM libraries together
# mkdir $VolumeRoot/Deploy/xz-5.4.6/lib
# lipo -create $VolumeRoot/Deploy/xz-5.4.6/lib_x64/liblzma.a $VolumeRoot/Deploy/xz-5.4.6/lib_arm/liblzma.a -output $VolumeRoot/Deploy/xz-5.4.6/lib/liblzma.a
# - Export the variables to make it easy (adjust the versions)
# export SSL_HOME=$VolumeRoot/Deploy/OpenSSL/1.1.1t/openssl
# export ZLIB_HOME=$VolumeRoot/Deploy/zlib/1.3
# export LZMA_HOME=$VolumeRoot/Deploy/xz-5.4.6
# - Go to the source directory '$VolumeRoot/Build/python-3.11.8' and run the configure command (might need some fixes if you updated any of the relative paths)
# ./configure --prefix=$VolumeRoot/Deploy/Python3.11.8 --enable-shared --enable-universalsdk=`xcrun --sdk macosx --show-sdk-path` --with-universal-archs=universal2 --enable-optimizations CFLAGS="-isysroot `xcrun --sdk macosx --show-sdk-path` -mmacosx-version-min=11.0 -gdwarf-2 -I$ZLIB_HOME/include -I$LZMA_HOME/include" CPPFLAGS="-mmacosx-version-min=11.0 -gdwarf-2 -I$ZLIB_HOME/include -I$LZMA_HOME/include" LDFLAGS="$ZLIB_HOME/lib/Mac/Release/libz.a $LZMA_HOME/lib/liblzma.a -mmacosx-version-min=11.0" --with-openssl=$SSL_HOME
# make -j28
# make install
# - Adjust the variables below according to your setup.
# - Run this scripts. It copies the binaries and fix their install name.
#
# To statically link OpenSSL, add the following to Modules/Setup.local in the python source location after running ./configure, but before make
#OPENSSL_INCLUDES=-I$(SSL_HOME)/include
#OPENSSL_LDFLAGS=-L$(SSL_HOME)/lib
# _ssl _ssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \
# -lssl -Wl,-hidden-lssl \
# -lcrypto -Wl,-hidden-lcrypto
# _hashlib _hashopenssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \
# -lcrypto -Wl,-hidden-lcrypto
#
# --------------------------------------------------------------------------------
# Install names
# --------------------------------------------------------------------------------
# On Mac, executables and dynamic libraries (.dylib, .so) contains install name ids to locate their dependencies. Because
# we copy, move and redistribute Python, the stored ids, which are paths, should be relative, so the end-user can put UE5 anywhere on
# his system. Unfortunately, the build system set hardcoded path. I didn't figure out how to pass the correct options to ./configure
# and/or make install to make them relative. So it is done by this script before copying in UE5 tree.
#
# To view a binary dependencies, we can use 'otool'
#
# otool -L python3.9 -> View the dynamic library install name ids loaded by the executable. (ids are paths)
# otool -l python3.9 -> View the LC_RPATH (rpath stored in the executable)
# otool -L libpython3.9.dylib -> View this dynamic library install name id and the dynamic librairy install name ids loaded by this libraries.
# otool -L hashlib.so -> View this dynamic library install name id and the dynamic librairy install name ids loaded by this libraries.
#
# To support relocating python elsewhere, we need to update the install name ids (the path) with tokens that are going to
# be pattern matched at load time. For executable like UnrealEditor or python3.9, we need to tell the dynamic loader where
# to look for the libraries, relative to the executable. An executable can have serveral "rpath" values. For python3.9
# executable, we want to add this one:
#
# install_name_tool -add_rpath @executable_path/../lib python3.9
#
# This gives one possible value to the token "@rpath" that we will used when searching the dependent librairies. Now
# we want to replace the libraries install name ids loaded by python3.9 executable to be relative too. This can be done as following:
#
# install_name_tool -change /Users/devqa/.pyenv/versions/3.9.7/lib/libpython3.9.dylib @rpath/libpython3.9.dylib python3.9
#
# The dynamic loader matches the install name id stored in the executable and the install name id of the library, so we want to
# change the libpython3.9.dylib install name id to something relative too:
#
# install_name_tool -id @rpath/libpython3.9.dylib Engine/Binaries/ThirdParty/Python3/Mac/lib/python3.9/libpython3.9.dylib
#
# The tokens '@rpath' and '@executable_path' are going to be replaced by the dynamic loader to locate files and then match
# the install name ids relative to the executable.
#
# The libpython3.9.dylib is loaded by UnrealEditor through a plugin. The UnrealEditor rpaths ensure we finds the plugin library
# first, then the plugin libraries refers to libpython3.11.dylib properly.
#
# The python core stuff is in libpython3.9.dylib, but several libraries are loaded 'on demand' when the intepreter encounter an import
# statement. Those dependencies are not visible by looking at libpython3.9.dylib. We need to be careful to ensure the module libraies
# are also correctly referred.
#
# --------------------------------------------------------------------------------
# Python tests suite (before running this script)
# --------------------------------------------------------------------------------
# Run Python unit tests (just after make install, before copying to UE tree). Those
# are the standard regression tests and they can be used to figure out if our build
# works outside the engine.
#
# WARNING: Few tests are sensible to the binary install names and from the root
# folder where the tests are invoked. From my understanding, this is just
# some limitations in the test itself.
#
# NOTE 1: When the full test suite ends, you get a summary. You will get the list of
# tests the passed, failed and the ones that were skipped. Review carefully
# the skipped tests list. Those were skipped because we didn't build the
# dependencies, because we didn't enable the resources (see below) or because
# they are platform specific. (I tried to run them all and check)
#
# NOTE 2: Some tests are disabled by default. If you scan the list of tests that
# were skipped, you can run some of them manually by enabling the 'resource'
# it needs with the -uall option. Bewared that some tests might be disabled
# for good reason, like the test_zipfile64 that use large amount of disk.
#
# Run the entire test suite from the binary install dir:
# cd $VolumeRoot/Deploy/Python3.11.8/bin
# ./python3.11 -m test
#
# To run a specific test:
# ./python3.11 -m test test_venv
#
# To run a specific test with verbose:
# ./python3.11 -m test -v test_venv
#
# To run a specific tests that was disabled becaue the resource wasn't enabled:
# See https://docs.python.org/3/library/test.html for more options.
# ./python3.11 -m test -uall -v test_socketserver
#
# --------------------------------------------------------------------------------
# Testing (after running this script)
# --------------------------------------------------------------------------------
#
# - Ensure the produced binaries are universal (Intel/Apple Sillicon ARM)
# - Run: lipo -archs Engine/Binaries/ThirdParty/Python3/Mac/lib/libpython3.11.dylib
# - Check for dependencies.
# - $:> cd Engine/Binaries/ThridParty/Python3/Mac/lib/python3.11/lib-dynload
# - $:> otool -L *.so'
# - Check the libraries dependencies for:
# - No dependencies are in /opt/usr/local -> Those are MacPort/Homebrew that user will likely not have.
# - Delete the $VolumeRoot/Deploy/Python3.11.8 -> This will ensure UE distribution doesn't have lib dependencies there.
# - Run the binary we plan to distribute as: Engine/Binaries/ThirdParty/Python3/Mac/bin/python3.11 --version
# - Run the unit tests distributed with python:
# - $:> cd Engine/Binaries/ThirdParty/Python3/Mac/lib/python3.11/test
# - $:> ../../../bin/python3.11 -m unittest discover
# - Launch UE5 EngineTest project
# - Go to menu Tools -> Test Automation
# - Search for python to discover all tests
# - Run all the tests (it is a good idea to run the test before the update to see if we have regression)
# --------------------------------------------------------------------------------
# Plugins
# --------------------------------------------------------------------------------
# - Some UE5 plugins likely needs to be rebuild when upgrading a minor version (3.7 to 3.9 for example)
# - USDImporter is one such plugin.
# - ML Deformer relies on PyTorch and may need some care.
#
# --------------------------------------------------------------------------------
# Tips:
# --------------------------------------------------------------------------------
# - To see all possible configure options: run ./configure --help
# - Read Mac/README.rst
# --------------------------------------------------------------------------------
python_src_dest_dir="`dirname \"$0\"`/Mac"
python_bin_dest_dir="`dirname \"$0\"`/../../../Binaries/ThirdParty/Python3/Mac"
python_bin_lib_dest_dir="$python_bin_dest_dir"/lib
python_src_dir="$VolumeRoot/Deploy/Python3.11.8"
python_exe_name=python3.11
python_lib_name=libpython3.11.dylib
if [ -d "$python_src_dir" ]
then
#
# Fixing up install names.
#
echo "Adding rpath to $python_exe_name."
install_name_tool -add_rpath @executable_path/../lib "$python_src_dir"/bin/$python_exe_name
echo "Fixing $python_exe_name dependencies: $python_src_dir/lib/$python_lib_name -> @rpath/$python_lib_name"
install_name_tool -change "$python_src_dir"/lib/$python_lib_name @rpath/$python_lib_name "$python_src_dir"/bin/$python_exe_name
echo "Fixing $python_lib_name install name id"
install_name_tool -id @rpath/$python_lib_name "$python_src_dir"/lib/$python_lib_name
#
# Deleting exiting destination folders from UE tree.
#
if [ -d "$python_src_dest_dir" ]
then
echo "Removing Existing Target Directory: $python_src_dest_dir"
rm -rf "$python_src_dest_dir"
fi
if [ -d "$python_bin_dest_dir" ]
then
echo "Removing Existing Target Directory: $python_bin_dest_dir"
rm -rf "$python_bin_dest_dir"
fi
#
# Copying local python intallation into UE tree.
#
echo "Copying Python: $python_src_dir"
mkdir -p "$python_src_dest_dir"/include
mkdir -p "$python_bin_dest_dir"/bin
mkdir -p "$python_bin_lib_dest_dir"
cp -R "$python_src_dir"/include/python3.11/* "$python_src_dest_dir"/include
cp -R "$python_src_dir"/bin/* "$python_bin_dest_dir"/bin
cp -R "$python_src_dir"/lib/* "$python_bin_lib_dest_dir"
cp -R "$python_src_dir"/lib/$python_lib_name "$python_bin_dest_dir"
chmod 755 "$python_bin_dest_dir"/$python_lib_name
#
# Copy TPS file back.
#
# if [ -f "$python_src_dest_dir"/../../../../Restricted/NoRedist/Source/ThirdParty/Python3/TPS/PythonMacBin.tps ]
# then
# cp -R "$python_src_dest_dir"/../../../../Restricted/NoRedist/Source/ThirdParty/Python3/TPS/PythonMacBin.tps "$python_bin_dest_dir"/
# fi
#
# Remove all symlinks (not a cross-platform thing).
#
echo "Processing Python symlinks: $python_dest_dir"
function remove_symlinks()
{
for file in $1/*
do
if [ -L "$file" ]
then
resolved_file="$1/`readlink \"$file\"`"
trimmed_file=".${file:$2}"
trimmed_resolved_file=".${resolved_file:$2}"
if [ -f "$resolved_file" ]
then
# for debugging
#echo " Removing symlink: $file -> $resolved_file"
echo " Removing symlink: $trimmed_file -> $trimmed_resolved_file"
rm -f "$file"
cp -R "$resolved_file" "$file"
else
echo "WARNING NOT FOUND: $resolved_file:"
fi
fi
if [ -d "$file" ]
then
remove_symlinks "$file" $2
fi
done
}
remove_symlinks "$python_bin_lib_dest_dir" ${#python_bin_lib_dest_dir}
function process_symlinks()
{
for file in $1/*
do
if [ -L "$file" ]
then
resolved_file="$1/`readlink \"$file\"`"
trimmed_file=".${file:$2}"
trimmed_resolved_file=".${resolved_file:$2}"
if [ -f "$resolved_file" ]
then
# for debugging
#echo " Processing symlink: $file -> $resolved_file"
echo " Processing symlink: $trimmed_file -> $trimmed_resolved_file"
rm -f "$file"
cp "$resolved_file" "$file"
else
echo "WARNING NOT FOUND: $resolved_file:"
fi
fi
if [ -d "$file" ]
then
process_symlinks "$file" $2
fi
done
}
process_symlinks "$python_bin_dest_dir" ${#python_bin_dest_dir}
#
# Remove any temporary files that were moved
#
function remove_obj_files()
{
for file in $1/*
do
if [ "${file}" != "${file%.pyc}" ] || [ "${file}" != "${file%.pyo}" ]
then
trimmed_file=".${file:$2}"
#echo " Removing: $trimmed_file"
rm -f "$file"
fi
if [ -d "$file" ]
then
remove_obj_files "$file" $2
fi
done
}
remove_obj_files "$python_bin_lib_dest_dir" ${#python_bin_lib_dest_dir}
function copy_openssl_libs()
{
# this was needed when using latest python3.9, their latest hashlib
# uses some of libssl and libcrypto functions (instead of their own anymore)
# TODO: see if this can be statically linked next time
# might need a peek at lib*.x.y.z.dylib for the actual hard coded path
openssl_lib_dir=/usr/local/opt/openssl/lib
# saving these instructions here on how to do this for future reference
cp "$openssl_lib_dir"/libssl.1.0.0.dylib "$python_bin_dest_dir"
cp "$openssl_lib_dir"/libcrypto.1.0.0.dylib "$python_bin_dest_dir"
install_name_tool -id "@rpath/libssl.1.0.0.dylib" "$python_bin_dest_dir"/libssl.1.0.0.dylib
install_name_tool -change "$openssl_lib_dir/libcrypto.1.0.0.dylib" "@executable_path/../libcrypto.1.0.0.dylib" "$python_bin_dest_dir"/libssl.1.0.0.dylib
install_name_tool -id "@rpath/libcrypto.1.0.0.dylib" "$python_bin_dest_dir"/libcrypto.1.0.0.dylib
# finally:
install_name_tool -change "$openssl_lib_dir/libssl.1.0.0.dylib" "@executable_path/../libssl.1.0.0.dylib" "$python_bin_dest_dir"/lib/python3.11/lib-dynload/_hashlib.so
install_name_tool -change "$openssl_lib_dir/libcrypto.1.0.0.dylib" "@executable_path/../libcrypto.1.0.0.dylib" "$python_bin_dest_dir"/lib/python3.11/lib-dynload/_hashlib.so
}
# disabling this - again, for future reference
#copy_openssl_libs
else
echo "Python Source Directory Missing: $python_src_dir"
fi
if [ ! -f "$python_src_dest_dir"/../../../../Restricted/NoRedist/Source/ThirdParty/Python/TPS/PythonMacBin.tps ]
then
echo "."
echo "WARNING: restore (i.e. revert) deleted $python_bin_dest_dir/PythonMacBin.tps before checking in"
echo "."
fi