macOS: bundle PySide6 and create a universal2 app

- macOS build: add arch checker for the bundle
- Actions macOS: restrict env to universal2 step
- Actions macOS: universal2 for zope.interface
- Actions macOS: universal2 for cffi and cryptography
- macOS build: ad-hoc sign again bundle after altering it
- macOS build: symlink instead of copy libshiboken6
- vendor.Qt: restrict warnings to QT_VERBOSE
- GHA: re-enable all jobs
- GHA macOS: use Python installer to build universal2 app
- Actions macOS: build Python with pyenv
- Older pyenv version is used...
- Abandon pyenv action
- Bump image to macOS-12
- One more attempt with pyenv
- Try with the official Python installer
- Must run as root
- Switch back to python3 calls
- Manually update PATH
- Momentarily disable other jobs
- Restore app cleaner
- ui: add support for PySide6
- vendor.Qt: "support" PySide6
- Update vendored Qt.py to 1.3.7
- Update requirements_gui
This commit is contained in:
Alberto Sottile 2022-11-05 23:40:30 +01:00
parent e9f506f713
commit 2aa73122ab
11 changed files with 887 additions and 360 deletions

View File

@ -63,15 +63,16 @@ jobs:
macos:
name: Build for macOS
runs-on: macos-10.15
runs-on: macos-12
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
run: |
wget https://www.python.org/ftp/python/3.10.6/python-3.10.6-macos11.pkg
sudo installer -verbose -pkg ./python-3.10.6-macos11.pkg -target /
echo "/Library/Frameworks/Python.framework/Versions/3.10/bin" >> $GITHUB_PATH
- name: Check Python install
run: |
@ -79,44 +80,41 @@ jobs:
python3 --version
which pip3
pip3 --version
file python3
- name: Install Python dependencies
run: |
pip3 install -U setuptools==60.* wheel pip
pip3 install twisted[tls] appnope requests certifi
pip3 install shiboken2==5.15.2 pyside2==5.15.2
pip3 install altgraph modulegraph macholib
pip3 install -U pip setuptools wheel
pip3 install -r requirements.txt
pip3 install -r requirements_gui.txt
pip3 install py2app
- name: Install py2app
- name: Install universal2 dependencies
env:
CFLAGS: -arch x86_64 -arch arm64
ARCHFLAGS: -arch x86_64 -arch arm64
run: |
git clone https://github.com/albertosottile/py2app.git
cd py2app
git checkout stubs
cd py2app/apptemplate
python3 setup.py build
cd ../..
python3 setup.py build
python3 setup.py install
cd ..
pip3 uninstall zope.interface -y
pip3 install --no-binary :all: zope.interface
pip3 uninstall cffi -y
pip3 install --no-binary :all: cffi
pip3 uninstall cryptography -y
pip3 download --platform macosx_10_10_universal2 --only-binary :all: --no-deps --dest . cryptography
pip3 install --no-cache-dir --no-index --find-links . cryptography
- name: Check Python dependencies
run: |
python3 -c "from PySide2 import __version__; print(__version__)"
python3 -c "from PySide2.QtCore import __version__; print(__version__)"
python3 -c "from PySide2.QtCore import QLibraryInfo; print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))"
python3 -c "from PySide6 import __version__; print(__version__)"
python3 -c "from PySide6.QtCore import __version__; print(__version__)"
python3 -c "from PySide6.QtCore import QLibraryInfo; print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))"
python3 -c "import ssl; print(ssl)"
python3 -c "from py2app.recipes import pyside2"
echo $DYLD_LIBRARY_PATH
echo $DYLD_FRAMEWORK_PATH
# python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LDLIBRARY"))'
python3 -c "from py2app.recipes import pyside6"
python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LDLIBRARY"))'
- name: Build
run: |
python3 ci/pyside2_linker.py
# export LIBPYTHON_FOLDER="$(python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LIBDIR"))')"
# ln -s $LIBPYTHON_FOLDER/libpython3.9m.dylib $LIBPYTHON_FOLDER/libpython3.9.dylib
export DYLD_FRAMEWORK_PATH="$(python3 -c 'from PySide2.QtCore import QLibraryInfo; print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))')"
export DYLD_LIBRARY_PATH="$(python3 -c 'import os.path, PySide2; print(os.path.dirname(PySide2.__file__))'):$(python3 -c 'import os.path, shiboken2; print(os.path.dirname(shiboken2.__file__))')"
python3 buildPy2app.py py2app
- name: Prepare for deployment

View File

@ -3,7 +3,7 @@
set -ex
python3 ci/macos_app_cleaner.py
cp dist/Syncplay.app/Contents/Resources/qt.conf dist/Syncplay.app/Contents/MacOS/
python3 ci/macos_app_arch_check.py
mkdir dist/Syncplay.app/Contents/Resources/English.lproj
mkdir dist/Syncplay.app/Contents/Resources/en_AU.lproj
@ -14,6 +14,8 @@ mkdir dist/Syncplay.app/Contents/Resources/ru.lproj
mkdir dist/Syncplay.app/Contents/Resources/Spanish.lproj
mkdir dist/Syncplay.app/Contents/Resources/es_419.lproj
python3 ci/macos_app_adhoc_sign.py
pip3 install dmgbuild
mv syncplay/resources/macOS_readme.pdf syncplay/resources/.macOS_readme.pdf

View File

@ -0,0 +1,5 @@
from py2app.util import codesign_adhoc
APPDIR = 'dist/Syncplay.app'
codesign_adhoc(APPDIR)

View File

@ -0,0 +1,41 @@
import glob
import subprocess
from typing import List
def run_external_command(command: List[str], print_output: bool = True) -> str:
"""Wrapper to ease the use of calling external programs"""
process = subprocess.Popen(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output, _ = process.communicate()
ret = process.wait()
if (output and print_output) or ret != 0:
print(output)
if ret != 0:
raise RuntimeError("Command returned non-zero exit code %s!" % ret)
return output
def arch_checker(path: str) -> bool:
no_error_found = True
for bin_to_check in glob.glob(path, recursive=True):
file_output = run_external_command(["file", bin_to_check], print_output=False)
if not ("x86_64" in file_output and "arm64" in file_output):
print(f"Non-universal2 binary found! - {bin_to_check}")
no_error_found = False
return no_error_found
def analyze(bundle_path: str) -> None:
valid = all([arch_checker(f"{bundle_path}/Contents/Frameworks/**/*.dylib"),
arch_checker(f"{bundle_path}/Contents/Frameworks/**/*.so"),
arch_checker(f"{bundle_path}/Contents/Resources/lib/**/*.dylib"),
arch_checker(f"{bundle_path}/Contents/Resources/lib/**/*.so"),
])
if valid:
print(f"The analyzed bundle '{bundle_path}' is universal2.")
else:
raise RuntimeError("The analyzed bundle is NOT universal2!")
def main():
analyze("Syncplay.app")
if __name__ == "__main__":
main()

View File

@ -1,73 +1,114 @@
import os
import platform
import shutil
import zipfile
from glob import glob
pyver = platform.python_version_tuple()[0] + platform.python_version_tuple()[1]
pyver = platform.python_version_tuple()[0] + '.' + platform.python_version_tuple()[1]
# clean Python library zip archive
# Clean resources
PATH = 'dist/Syncplay.app/Contents/Resources/lib'
PATH = 'dist/Syncplay.app/Contents/Resources/'
zin = zipfile.ZipFile(f'{PATH}/python{pyver}.zip', 'r')
tbd = [path for path in zin.namelist() if 'PySide2/Qt/' in path]
zout = zipfile.ZipFile(f'{PATH}/python{pyver}_new.zip', 'w', zipfile.ZIP_DEFLATED)
for item in zin.namelist():
buffer = zin.read(item)
if item not in tbd:
zout.writestr(item, buffer)
zout.close()
zin.close()
os.remove(f'{PATH}/python{pyver}.zip')
os.rename(f'{PATH}/python{pyver}_new.zip', f'{PATH}/python{pyver}.zip')
# clean Frameworks folder
PATH = 'dist/Syncplay.app/Contents/Frameworks'
to_be_kept = ['QtCore', 'QtDBus', 'QtGui', 'QtNetwork', 'QtPrintSupport', 'QtQml', 'QtWidgets']
to_be_kept = []
to_be_deleted = []
for f in glob(f'{PATH}/Qt*'):
for f in glob(f'{PATH}/qt*'):
if not any({k in f for k in to_be_kept}):
to_be_deleted.append(f)
for p in to_be_deleted:
if os.path.exists(p):
if os.path.isdir(p):
shutil.rmtree(p, ignore_errors=True)
else:
os.remove(p)
# Clean PySide2 folder
# Clean PySide6 folder
PATH = 'dist/Syncplay.app/Contents/Resources/lib/python3.9/PySide2'
PATH = f'dist/Syncplay.app/Contents/Resources/lib/python{pyver}/PySide6'
shutil.rmtree(f'{PATH}/examples', ignore_errors=True)
shutil.rmtree(f'{PATH}/include', ignore_errors=True)
to_be_kept = ['QtCore', 'QtDBus', 'QtGui', 'QtNetwork', 'QtPrintSupport', 'QtQml', 'QtWidgets']
to_be_kept = ['QtCore', 'QtGui', 'QtWidgets']
to_be_deleted = []
for f in glob(f'{PATH}/Qt*'):
if not any({k in f for k in to_be_kept}):
to_be_deleted.append(f)
for a in glob(f'{PATH}/*.app'):
to_be_deleted.append(a)
to_be_deleted.remove(f'{PATH}/Qt')
to_be_deleted.extend([f'{PATH}/lupdate', f'{PATH}/qmllint', f'{PATH}/lrelease'])
for p in to_be_deleted:
if os.path.exists(p):
if os.path.isdir(p):
shutil.rmtree(p, ignore_errors=True)
else:
os.remove(p)
# move .so from Framework to PySide2 folder
# Clean PySide6/Qt folder
FROM = 'dist/Syncplay.app/Contents/Frameworks'
TO = 'dist/Syncplay.app/Contents/Resources/lib/python3.9/PySide2'
PATH = f'dist/Syncplay.app/Contents/Resources/lib/python{pyver}/PySide6/Qt'
for f in glob(f'{FROM}/Qt*.so'):
fn = os.path.basename(f)
shutil.move(f, f'{TO}/{fn}')
to_be_deleted.extend([f'{PATH}/qml', f'{PATH}/translations'])
for p in to_be_deleted:
if os.path.exists(p):
if os.path.isdir(p):
shutil.rmtree(p, ignore_errors=True)
else:
os.remove(p)
# Clean PySide6/Qt/lib folder
PATH = f'dist/Syncplay.app/Contents/Resources/lib/python{pyver}/PySide6/Qt/lib'
to_be_kept = ['QtCore', 'QtDBus', 'QtGui', 'QtWidgets']
to_be_deleted = [f'{PATH}/metatypes']
for f in glob(f'{PATH}/Qt*'):
if not any({k in f for k in to_be_kept}):
to_be_deleted.append(f)
for p in to_be_deleted:
if os.path.exists(p):
if os.path.isdir(p):
shutil.rmtree(p, ignore_errors=True)
else:
os.remove(p)
# Clean PySide6/Qt/plugins folder
PATH = f'dist/Syncplay.app/Contents/Resources/lib/python{pyver}/PySide6/Qt/plugins'
to_be_kept = ['platforms', 'styles']
to_be_deleted = []
for f in glob(f'{PATH}/*'):
if not any({k in f for k in to_be_kept}):
to_be_deleted.append(f)
for p in to_be_deleted:
if os.path.exists(p):
if os.path.isdir(p):
shutil.rmtree(p, ignore_errors=True)
else:
os.remove(p)
# symlink .so from shiboken6 to PySide6 folder
cwd = os.getcwd()
FROM = f'dist/Syncplay.app/Contents/Resources/lib/python{pyver}/shiboken6'
TO = f'dist/Syncplay.app/Contents/Resources/lib/python{pyver}/PySide6'
fn = os.path.basename(glob(f'{FROM}/libshiboken6*.dylib')[0])
os.chdir(TO)
os.symlink(f'../shiboken6/{fn}', f'./{fn}')
os.chdir(cwd)

View File

@ -1,2 +1,3 @@
pyside2>=5.11.0
pyside2>=5.11.0; sys_platform != 'darwin'
pyside6; sys_platform == 'darwin'
requests>=2.20.0; sys_platform == 'darwin'

View File

@ -514,10 +514,10 @@ class ConfigurationGetter(object):
self._overrideConfigWithArgs(args)
if not self._config['noGui']:
try:
from syncplay.vendor.Qt import QtWidgets, IsPySide, IsPySide2, QtGui
from syncplay.vendor.Qt import QtWidgets, IsPySide, IsPySide2, IsPySide6, QtGui
from syncplay.vendor.Qt.QtCore import QCoreApplication
from syncplay.vendor import qt5reactor
if not (IsPySide2 or IsPySide):
if not (IsPySide6 or IsPySide2 or IsPySide):
raise ImportError
if QCoreApplication.instance() is None:
self.app = QtWidgets.QApplication(sys.argv)

View File

@ -11,7 +11,7 @@ from syncplay.players.playerFactory import PlayerFactory
from syncplay.utils import isBSD, isLinux, isMacOS, isWindows
from syncplay.utils import resourcespath, posixresourcespath, playerPathExists
from syncplay.vendor.Qt import QtCore, QtWidgets, QtGui, __binding__, IsPySide, IsPySide2
from syncplay.vendor.Qt import QtCore, QtWidgets, QtGui, __binding__, IsPySide, IsPySide2, IsPySide6
from syncplay.vendor.Qt.QtCore import Qt, QSettings, QCoreApplication, QSize, QPoint, QUrl, QLine, QEventLoop, Signal
from syncplay.vendor.Qt.QtWidgets import QApplication, QLineEdit, QLabel, QCheckBox, QButtonGroup, QRadioButton, QDoubleSpinBox, QPlainTextEdit
from syncplay.vendor.Qt.QtGui import QCursor, QIcon, QImage, QDesktopServices
@ -22,7 +22,9 @@ except AttributeError:
pass # To ignore error "Attribute Qt::AA_EnableHighDpiScaling must be set before QCoreApplication is created"
if hasattr(QtCore.Qt, 'AA_UseHighDpiPixmaps'):
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
if IsPySide2:
if IsPySide6:
from PySide6.QtCore import QStandardPaths
elif IsPySide2:
from PySide2.QtCore import QStandardPaths
@ -451,7 +453,7 @@ class ConfigDialog(QtWidgets.QDialog):
defaultdirectory = QDesktopServices.storageLocation(QDesktopServices.HomeLocation)
else:
defaultdirectory = ""
elif IsPySide2:
elif IsPySide6 or IsPySide2:
if self.config["mediaSearchDirectories"] and os.path.isdir(self.config["mediaSearchDirectories"][0]):
defaultdirectory = self.config["mediaSearchDirectories"][0]
elif os.path.isdir(self.mediadirectory):
@ -725,7 +727,10 @@ class ConfigDialog(QtWidgets.QDialog):
self.executablepathLabel.setObjectName("executable-path")
self.executablepathCombobox.setObjectName("executable-path")
self.executablepathCombobox.setMinimumContentsLength(constants.EXECUTABLE_COMBOBOX_MINIMUM_LENGTH)
if not IsPySide6:
self.executablepathCombobox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToMinimumContentsLength)
else:
self.executablepathCombobox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToMinimumContentsLengthWithIcon)
self.mediapathLabel.setObjectName("media-path")
self.mediapathTextbox.setObjectName(constants.LOAD_SAVE_MANUALLY_MARKER + "media-path")
self.playerargsLabel.setObjectName("player-arguments")

View File

@ -2,7 +2,7 @@ import os
if "QT_PREFERRED_BINDING" not in os.environ:
os.environ["QT_PREFERRED_BINDING"] = os.pathsep.join(
["PySide2", "PySide", "PyQt5", "PyQt4"]
["PySide6", "PySide2", "PySide", "PyQt5", "PyQt4"]
)
try:

View File

@ -19,7 +19,7 @@ from syncplay.utils import resourcespath
from syncplay.utils import isLinux, isWindows, isMacOS
from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize, isURL
from syncplay.vendor import Qt
from syncplay.vendor.Qt import QtCore, QtWidgets, QtGui, __binding__, __binding_version__, __qt_version__, IsPySide, IsPySide2
from syncplay.vendor.Qt import QtCore, QtWidgets, QtGui, __binding__, __binding_version__, __qt_version__, IsPySide, IsPySide2, IsPySide6
from syncplay.vendor.Qt.QtCore import Qt, QSettings, QSize, QPoint, QUrl, QLine, QDateTime
applyDPIScaling = True
if isLinux():
@ -33,7 +33,9 @@ except AttributeError:
pass # To ignore error "Attribute Qt::AA_EnableHighDpiScaling must be set before QCoreApplication is created"
if hasattr(QtCore.Qt, 'AA_UseHighDpiPixmaps'):
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, applyDPIScaling)
if IsPySide2:
if IsPySide6:
from PySide6.QtCore import QStandardPaths
elif IsPySide2:
from PySide2.QtCore import QStandardPaths
if isMacOS() and IsPySide:
from Foundation import NSURL
@ -828,7 +830,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.listTreeView.setFirstColumnSpanned(roomtocheck, self.listTreeView.rootIndex(), True)
roomtocheck += 1
self.listTreeView.header().setStretchLastSection(False)
if IsPySide2:
if IsPySide6 or IsPySide2:
self.listTreeView.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
self.listTreeView.header().setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
self.listTreeView.header().setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
@ -842,7 +844,7 @@ class MainWindow(QtWidgets.QMainWindow):
if self.listTreeView.header().width() < (NarrowTabsWidth+self.listTreeView.header().sectionSize(3)):
self.listTreeView.header().resizeSection(3, self.listTreeView.header().width()-NarrowTabsWidth)
else:
if IsPySide2:
if IsPySide6 or IsPySide2:
self.listTreeView.header().setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch)
if IsPySide:
self.listTreeView.header().setResizeMode(3, QtWidgets.QHeaderView.Stretch)
@ -1026,7 +1028,7 @@ class MainWindow(QtWidgets.QMainWindow):
defaultdirectory = QtGui.QDesktopServices.storageLocation(QtGui.QDesktopServices.HomeLocation)
else:
defaultdirectory = ""
elif IsPySide2:
elif IsPySide6 or IsPySide2:
if self.config["mediaSearchDirectories"] and os.path.isdir(self.config["mediaSearchDirectories"][0]) and includeUserSpecifiedDirectories:
defaultdirectory = self.config["mediaSearchDirectories"][0]
elif includeUserSpecifiedDirectories and os.path.isdir(self.mediadirectory):
@ -2058,7 +2060,10 @@ class MainWindow(QtWidgets.QMainWindow):
settings.beginGroup("MainWindow")
self.resize(settings.value("size", QSize(700, 500)))
movePos = settings.value("pos", QPoint(200, 200))
if not IsPySide6:
windowGeometry = QtWidgets.QApplication.desktop().availableGeometry(self)
else:
windowGeometry = QtWidgets.QApplication.primaryScreen().geometry()
posIsOnScreen = windowGeometry.contains(QtCore.QRect(movePos.x(), movePos.y(), 1, 1))
if not posIsOnScreen:
movePos = QPoint(200,200)

957
syncplay/vendor/Qt.py vendored Executable file → Normal file

File diff suppressed because it is too large Load Diff