-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Improvements to generated app bundles on macOS #7191
Description
Description of the issue
This issue is a collection of problems with the app bundles that pyinstall builds for macOS.
It is meant to point out things that could easily be fixed and would result in cleaner, actually usable app bundles:
- removing all RPATH entries is not what you want to do
- it seems like the right thing to do but you really only want to remove absolute RPATHs (not pointing to system libraries) and convert them to
@rpathrelatives - this goes for all dependencies off all binary python modules as well as the interpreter and wrapper itself
@executable_path/../Frameworksand optionally@executable_pathshould be added viainstall_name_tool -add_rpath
- this plays together with the point 1) as it allows the dynamic linker to look in the executable's directory as well as in the relative Frameworks directory and actually find dylibs there. What currently happens if you have a binary module that tries to load a dependency dynamically at runtime is that the frozen app will crash as there are no known paths to look for that dependency and dyld does not look in the current directory by default.
- Contents/MacOS should contain only binaries and symlinks
- this mainly concerns apps that want to be notarised (most macOS apps these days) or be listed in the Mac AppStore
- it's consistent with the bundle layout for most native apps
- (after 3 is implemented) the PyInstall bootstrapper needs to set PYTHONPATH to include
@executable_dir/../Frameworks
- this step would remove the requirement to symlink directories and .so files from
../Frameworksback to the bin directory
Here's a comparison of the layout of a simply PyInstaller Qt6 bundle and a bundle modified to include the suggested changes:
- plain PyInstaller output:
qt6demo2.app/Contents
qt6demo2.app/Contents/Frameworks
qt6demo2.app/Contents/Info.plist
qt6demo2.app/Contents/MacOS
qt6demo2.app/Contents/MacOS/PySide6
... [all of PySide6's contents]
qt6demo2.app/Contents/MacOS/Python
qt6demo2.app/Contents/MacOS/QtCore
qt6demo2.app/Contents/MacOS/QtDBus
qt6demo2.app/Contents/MacOS/QtGui
qt6demo2.app/Contents/MacOS/QtNetwork
qt6demo2.app/Contents/MacOS/QtOpenGL
qt6demo2.app/Contents/MacOS/QtQml
qt6demo2.app/Contents/MacOS/QtQmlModels
qt6demo2.app/Contents/MacOS/QtQuick
qt6demo2.app/Contents/MacOS/QtSvg
qt6demo2.app/Contents/MacOS/QtVirtualKeyboard
qt6demo2.app/Contents/MacOS/QtWidgets
qt6demo2.app/Contents/MacOS/base_library.zip -> ../Resources/base_library.zip
qt6demo2.app/Contents/MacOS/lib-dynload
qt6demo2.app/Contents/MacOS/lib-dynload/_bisect.cpython-310-darwin.so
... [all of Python's native module so files]
qt6demo2.app/Contents/MacOS/lib-dynload/zlib.cpython-310-darwin.so
qt6demo2.app/Contents/MacOS/libcrypto.1.1.dylib
qt6demo2.app/Contents/MacOS/liblzma.5.dylib
qt6demo2.app/Contents/MacOS/libmpdec.2.5.1.dylib
qt6demo2.app/Contents/MacOS/libpyside6.abi3.6.3.dylib
qt6demo2.app/Contents/MacOS/libshiboken6.abi3.6.3.dylib
qt6demo2.app/Contents/MacOS/libssl.1.1.dylib
qt6demo2.app/Contents/MacOS/qt6demo2
qt6demo2.app/Contents/MacOS/shiboken6
qt6demo2.app/Contents/MacOS/shiboken6/Shiboken.abi3.so
qt6demo2.app/Contents/Resources
qt6demo2.app/Contents/Resources/base_library.zip
qt6demo2.app/Contents/Resources/icon-windowed.icns
qt6demo2.app/Contents/_CodeSignature
qt6demo2.app/Contents/_CodeSignature/CodeResources
$ otool -l qt6demo2.app/Contents/MacOS/qt6demo2 | grep -b3 RPATH
<empty>
$ otool -l qt6demo2.app/Contents/MacOS/Python | grep -b3 RPATH
12194- current version 1311.100.3
12227-compatibility version 1.0.0
12255-Load command 14
12271: cmd LC_RPATH
12294- cmdsize 32
12311- path /opt/homebrew/lib (offset 12)
12355-Load command 15- same bundle but with fixed locations for shared libraries and runtime data + fixed rpaths:
qt6demo2.app
qt6demo2.app/Contents
qt6demo2.app/Contents/Frameworks
qt6demo2.app/Contents/Frameworks/PySide6
... [all of PySide6's contents]
qt6demo2.app/Contents/Frameworks/lib-dynload
qt6demo2.app/Contents/Frameworks/lib-dynload/_bisect.cpython-310-darwin.so
... [all of Python's native module so files]
qt6demo2.app/Contents/Frameworks/lib-dynload/_blake2.cpython-310-darwin.so
qt6demo2.app/Contents/Frameworks/libcrypto.1.1.dylib
qt6demo2.app/Contents/Frameworks/liblzma.5.dylib
qt6demo2.app/Contents/Frameworks/libmpdec.2.5.1.dylib
qt6demo2.app/Contents/Frameworks/libpyside6.abi3.6.3.dylib
qt6demo2.app/Contents/Frameworks/libshiboken6.abi3.6.3.dylib
qt6demo2.app/Contents/Frameworks/libssl.1.1.dylib
qt6demo2.app/Contents/Frameworks/shiboken6
qt6demo2.app/Contents/Frameworks/shiboken6/Shiboken.abi3.so
qt6demo2.app/Contents/Info.plist
qt6demo2.app/Contents/MacOS
qt6demo2.app/Contents/MacOS/qt6demo2
qt6demo2.app/Contents/MacOS/PySide6 -> ../Frameworks/PySide6
qt6demo2.app/Contents/MacOS/Python
qt6demo2.app/Contents/MacOS/base_library.zip -> ../Resources/base_library.zip
qt6demo2.app/Contents/MacOS/lib-dynload -> ../Frameworks/lib-dynload
qt6demo2.app/Contents/MacOS/shiboken6 -> ../Frameworks/shiboken6
qt6demo2.app/Contents/Resources
qt6demo2.app/Contents/Resources/base_library.zip
qt6demo2.app/Contents/Resources/icon-windowed.icns
qt6demo2.app/Contents/_CodeSignature
qt6demo2.app/Contents/_CodeSignature/CodeDirectory
qt6demo2.app/Contents/_CodeSignature/CodeRequirements
qt6demo2.app/Contents/_CodeSignature/CodeRequirements-1
qt6demo2.app/Contents/_CodeSignature/CodeResources
qt6demo2.app/Contents/_CodeSignature/CodeSignature
$ otool -l qt6demo2.app/Contents/MacOS/qt6demo2 | grep -b3 RPATH
6641- dataoff 970848
6658- datasize 25888
6674-Load command 22
6690: cmd LC_RPATH
6713- cmdsize 32
6730- path @executable_path (offset 12)
6773-Load command 23
6789: cmd LC_RPATH
6812- cmdsize 48
6829- path @executable_path/../Frameworks/ (offset 12)
$ otool -l qt6demo2.app/Contents/MacOS/qt6demo2 | grep -b3 RPATH
12407- current version 1311.100.3
12440-compatibility version 1.0.0
12468-Load command 14
12484: cmd LC_RPATH
12507- cmdsize 32
12524- path /opt/homebrew/lib (offset 12)
12568-Load command 15
--
12801- dataoff 3609200
12819- datasize 46480
12835-Load command 18
12851: cmd LC_RPATH
12874- cmdsize 32
12891- path @executable_path (offset 12)
12934-Load command 19
12950: cmd LC_RPATH
12973- cmdsize 48
12990- path @executable_path/../Frameworks/ (offset 12)And a simple bash script that shows the applied changes (which could be done by PyInstaller in the first place), not including PYTHONPATH:
#!/bin/zsh
if [ ! -d "$1" ] then
echo "usage: fix-py-bundle.sh <app-bundle>" >&2
exit 1
fi
WRAPPER_NAME="$(basename $1 .app)"
pushd "$1" > /dev/null
# move any shared objects and other dependencies to Frameworks
find Contents/MacOS/ -iname '*.dylib' -mindepth 0 -maxdepth 1 -exec mv '{}' Contents/Frameworks/ ';'
find Contents/MacOS/ -iname '*.so' -mindepth 0 -maxdepth 1 -exec mv '{}' Contents/Frameworks/ ';'
find Contents/MacOS/ -name 'Qt*' -mindepth 0 -maxdepth 1 -exec mv '{}' Contents/Frameworks/ ';'
find Contents/MacOS/ -type d -mindepth 1 -maxdepth 1 -exec mv '{}' Contents/Frameworks/ ';'
# add the missing rpath entries to actually look in Frameworks for shared objects
install_name_tool -add_rpath '@executable_path' "Contents/MacOS/$WRAPPER_NAME"
install_name_tool -add_rpath '@executable_path' Contents/MacOS/Python
install_name_tool -add_rpath '@executable_path/../Frameworks/' "Contents/MacOS/$WRAPPER_NAME"
install_name_tool -add_rpath '@executable_path/../Frameworks/' Contents/MacOS/Python
# create symlinks for all directories that got moved to Frameworks since we don't have influence on
# PYTHONPATH (would not be needed if it contained @executable_path/../Frameworks as well)
pushd Contents/MacOS/ > /dev/null
find ../Frameworks/ -type d -mindepth 1 -maxdepth 1 -exec ln -s '{}' . ';'
popd > /dev/null
# refresh the ad-hoc signature (could our actual signing profile here..)
codesign -s - -f --deep .
popd > /dev/nullI would have gladly tried to provide a PR with all of this but attempting to build PyInstaller on macOS is a hot mess ^^'.
Context information (for bug reports)
- Output of
pyinstaller --version:5.6.1 - Version of Python: 3.10.6
- Platform: OS X
- How you installed Python: brew
- Did you also try this on another platform? Does it work there? - not relevant