diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml new file mode 100644 index 0000000..830fd7a --- /dev/null +++ b/.github/workflows/default.yml @@ -0,0 +1,362 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "master" branch + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + ubuntu: + name: Build Ubuntu Debian package + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-20.04, ubuntu-22.04] + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.10.8' + + - name: Install prereqs + run: | + sudo apt install -y libegl-dev libxcb-keysyms1 libxcb-randr0 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-shape0 libxcb-xkb1 libxcb-render-util0 + + - name: Setup Env Vars + run: | + echo "BUILD_DIR=build" >> $GITHUB_ENV + echo "DIST_DIR=dist" >> $GITHUB_ENV + FILE_LOC=$(find . -name info.py) + export VER="$(cat $FILE_LOC | grep -w version | awk -F'"' '$0=$2')" + export PROJ_NAME="$(cat $FILE_LOC | grep -w name | awk -F'"' '$0=$2')" + export PROJ_MOD="$(cat $FILE_LOC | grep -w module_name | awk -F'"' '$0=$2')" + export MAIN_FILE="$(cat $FILE_LOC | grep -w main_file | awk -F'"' '$0=$2')" + export MAIN_MOD="$(cat $FILE_LOC | grep -w main_module | awk -F'"' '$0=$2')" + export BUNDLE_ID="$(cat $FILE_LOC | grep -w bundle_identifier | awk -F'"' '$0=$2')" + export EMAIL="$(cat $FILE_LOC | grep -w email | awk -F'"' '$0=$2')" + export DESCRIPTION="$(cat $FILE_LOC | grep -w description | awk -F'"' '$0=$2')" + echo "VER=$VER" >> $GITHUB_ENV + echo "DESCRIPTION=$DESCRIPTION" >> $GITHUB_ENV + echo "PROJECT_NAME=$PROJ_NAME" >> $GITHUB_ENV + echo "PROJ_DIR=$PROJ_MOD" >> $GITHUB_ENV + echo "PROJ_PATH=$PROJ_MOD" >> $GITHUB_ENV + echo "PROJ_MOD=$PROJ_MOD" >> $GITHUB_ENV + echo "MAIN_MOD=$MAIN_MOD" >> $GITHUB_ENV + echo "MAIN_FILE=$MAIN_FILE" >> $GITHUB_ENV + echo "BUNDLE_ID=$BUNDLE_ID" >> $GITHUB_ENV + echo "EMAIL=$EMAIL" >> $GITHUB_ENV + + - name: Check Python install + run: | + pip3 install --upgrade pip + python3 -m venv venv + source venv/bin/activate + + which python3 + python3 --version + which pip3 + pip3 --version + file python3 + + - name: Install Python dependencies + run: | + source venv/bin/activate + pip3 install -U pip setuptools wheel certifi + pip3 install -r requirements.txt + pip3 install py2app + PYINSTALLER_COMPILE_BOOTLOADER=1 MACOSX_DEPLOYMENT_TARGET=10.9 pip3 install https://github.com/pyinstaller/pyinstaller/tarball/develop --no-binary :all: + + - name: Check Python dependencies + run: | + source venv/bin/activate + 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 pyside6" + python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LDLIBRARY"))' + + - name: Setup Deb + run: | + source venv/bin/activate + python3 create_pyinstaller_file.py + pyinstaller -w --noconfirm --hidden-import PySide6 \ + --add-data "${{ env.PROJ_DIR }}/resources:." \ + --icon "${{ env.PROJ_DIR }}/resources/icon.png" \ + --hidden-import configobj \ + --workpath "${{ env.BUILD_DIR }}" \ + --distpath "${{ env.DIST_DIR }}" \ + -n "${{ env.PROJ_MOD }}" pyinstaller.py + mv "${{ env.DIST_DIR }}/${{ env.PROJ_MOD }}" "./${{ env.PROJ_MOD }}-tar" + tar -czvf ${{ env.PROJ_MOD }}.tar.gz ${{ env.PROJ_MOD }}-tar/* + ci/deb-script.sh + + - uses: jiro4989/build-deb-action@v2 + with: + package: ${{ env.PROJ_PATH }}-${{ env.VER }}-${{ matrix.os }}-deb + package_root: .debpkg + maintainer: ${{ env.EMAIL }} + version: ${{ env.VER }} # refs/tags/v*.*.* + arch: 'all' + depends: 'libc6 (>= 2.2.1)' + desc: "${{ env.DESCRIPTION }}" + + - name: Deploy tar + uses: actions/upload-artifact@v3 + with: + name: ${{ env.PROJ_PATH }}_${{ env.VER }}_${{ matrix.os }}-tar + path: | + ${{ env.PROJ_MOD }}.tar.gz + + - name: Deploy deb + uses: actions/upload-artifact@v3 + with: + name: ${{ env.PROJ_PATH }}_${{ env.VER }}_${{ matrix.os }}-deb + path: | + ./*.deb + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + generate_release_notes: true + files: | + ${{ env.PROJ_MOD }}.tar.gz + ./*.deb + + windows: + name: Build for Windows + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-2019] + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.10.8' + + - name: Check Python install + run: | + python -m venv venv + venv\Scripts\Activate.ps1 + which python + python --version + python -c "import struct; print(struct.calcsize('P') * 8)" + which pip + pip --version + + - name: Install Python dependencies + run: | + venv\Scripts\Activate.ps1 + pip install -U setuptools wheel pip pillow + pip install -r requirements.txt + pip install https://github.com/pyinstaller/pyinstaller/tarball/develop + + - name: Check Python dependencies + run: | + venv\Scripts\Activate.ps1 + python -c "from PySide6 import __version__; print(__version__)" + python -c "from PySide6.QtCore import __version__; print(__version__)" + python -c "from PySide6.QtCore import QLibraryInfo; print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))" + + - name: Setup Env Vars + run: | + $FILE_LOC = (Get-ChildItem -Path .\ -Filter info.py -Recurse -ErrorAction SilentlyContinue -Force | foreach {$_.FullName}) + $VER = (findstr /b version $FILE_LOC).split('"')[1] + echo "VER=$VER" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "BUILD_DIR=build" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "DIST_DIR=dist" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + $PROJ_NAME = (findstr /b name $FILE_LOC).split('"')[1] + $PROJ_MOD = (findstr /b module_name $FILE_LOC).split('"')[1] + $MAIN_FILE = (findstr /b main_file $FILE_LOC).split('"')[1] + $MAIN_MOD = (findstr /b main_mod $FILE_LOC).split('"')[1] + echo "PROJECT_NAME=$PROJ_NAME" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "PROJ_MOD=$PROJ_MOD" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "PROJ_DIR=$PROJ_MOD" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "MAIN_FILE=$MAIN_FILE" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + echo "MAIN_MOD=$MAIN_MOD" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Build + run: | + venv\Scripts\Activate.ps1 + python create_pyinstaller_file.py + pyinstaller -w --noconfirm --hidden-import PySide6 ` + --add-data "${{ env.PROJ_DIR }}\resources;." ` + -i ${{ env.PROJ_DIR }}\resources\icon.ico ` + --hidden-import pkg_resources ` + --workpath "${{ env.BUILD_DIR }}" ` + --distpath "${{ env.DIST_DIR }}" ` + --onedir -n "${{ env.PROJECT_NAME }}" pyinstaller.py + Compress-Archive -Path .\${{env.DIST_DIR}}\${{env.PROJECT_NAME}} -DestinationPath ${{env.PROJECT_NAME}}-${{ env.VER }}-windows-x64.zip + + + - name: Deploy Zip + uses: actions/upload-artifact@v3 + with: + name: ${{ env.PROJECT_NAME }}_${{ env.VER }}-${{ matrix.os }}-zip + path: | + ${{ env.PROJECT_NAME }}-${{ env.VER }}-windows-x64.zip + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + generate_release_notes: true + files: | + ${{ env.PROJECT_NAME }}-${{ env.VER }}-windows-x64.zip + + + macos: + name: Build for macOS + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-11, macos-12] + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Homebrew + run: | + NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + brew install create-dmg + - name: Setup Python + run: | + wget https://www.python.org/ftp/python/3.10.8/python-3.10.8-macos11.pkg + sudo installer -verbose -pkg ./python-3*.pkg -target / + echo "/Library/Frameworks/Python.framework/Versions/3.10/bin" >> $GITHUB_PATH + - name: Setup Env Vars + run: | + echo "BUILD_DIR=build" >> $GITHUB_ENV + echo "DIST_DIR=dist" >> $GITHUB_ENV + FILE_LOC=$(find . -name info.py) + export VER="$(cat $FILE_LOC | grep -w version | awk -F'"' '$0=$2')" + export PROJ_NAME="$(cat $FILE_LOC | grep -w name | awk -F'"' '$0=$2')" + export PROJ_MOD="$(cat $FILE_LOC | grep -w module_name | awk -F'"' '$0=$2')" + export MAIN_FILE="$(cat $FILE_LOC | grep -w main_file | awk -F'"' '$0=$2')" + export MAIN_MOD="$(cat $FILE_LOC | grep -w main_module | awk -F'"' '$0=$2')" + export BUNDLE_ID="$(cat $FILE_LOC | grep -w bundle_identifier | awk -F'"' '$0=$2')" + echo "VER=$VER" >> $GITHUB_ENV + echo "PROJECT_NAME=$PROJ_NAME" >> $GITHUB_ENV + echo "PROJ_DIR=$PROJ_MOD" >> $GITHUB_ENV + echo "PROJ_MOD=$PROJ_MOD" >> $GITHUB_ENV + echo "MAIN_FILE=$MAIN_FILE" >> $GITHUB_ENV + echo "MAIN_MOD=$MAIN_MOD" >> $GITHUB_ENV + echo "BUNDLE_ID=$BUNDLE_ID" >> $GITHUB_ENV + + - name: Check Python install + run: | + pip3 install --upgrade pip + python3 -m venv venv + source venv/bin/activate + which python3 + python3 --version + which pip3 + pip3 --version + file python3 + + - name: Install Python dependencies + run: | + source venv/bin/activate + pip3 install -U pip setuptools wheel pyclean + pip3 install -r requirements.txt + pip3 install py2app + PYINSTALLER_COMPILE_BOOTLOADER=1 MACOSX_DEPLOYMENT_TARGET=10.9 pip3 install https://github.com/pyinstaller/pyinstaller/tarball/develop --no-binary :all: + + - name: Install universal2 dependencies + env: + CFLAGS: -arch x86_64 -arch arm64 + ARCHFLAGS: -arch x86_64 -arch arm64 + run: | + source venv/bin/activate + 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: | + source venv/bin/activate + 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 pyside6" + python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LDLIBRARY"))' + - name: Build + run: | + source venv/bin/activate + python3 create_pyinstaller_file.py + # py2app works better + python3 buildPy2app.py py2app + # pyinstaller -w --noconfirm --hidden-import PySide6 \ + # --add-data "${{ env.PROJ_DIR }}/resources:." \ + # --icon "${{ env.PROJ_DIR }}/resources/icon.icns" \ + # --target-architecture universal2 \ + # --osx-bundle-identifier "${{ env.BUNDLE_ID }}" \ + # --hidden-import pkg_resources \ + # --distpath "${{ env.BUILD_DIR }}/ProjectMac" \ + # --onefile -n "${{ env.PROJECT_NAME }}" pyinstaller.py + mv "${{ env.DIST_DIR }}/${{ env.PROJECT_NAME }}.app" "${{ env.PROJECT_NAME }}.app" + # Clean the directory so it's not full of python bytecode + pyclean "${{ env.PROJECT_NAME }}.app" + python3 ci/cleandist.py "${{ env.PROJECT_NAME }}.app" + python3 ci/codesign.py "${{ env.PROJECT_NAME }}.app" + zip -ry "${{ env.PROJECT_NAME }}_${{ env.VER }}-macos-universal.zip" "${{ env.PROJECT_NAME }}.app" + + - name: Prepare for deployment + run: | + source venv/bin/activate + mkdir -p "${{ env.DIST_DIR }}/dmg" + test -f "${{ env.PROJECT_NAME }}_${{ env.VER }}.dmg" && rm "${{ env.PROJECT_NAME }}_${{ env.VER }}.dmg" + mv "${{ env.PROJECT_NAME }}.app" "${{ env.DIST_DIR}}/dmg/" + create-dmg \ + --volname "${{ env.PROJECT_NAME }}-${{ env.VER }} Installer" \ + --volicon "${{ env.PROJ_DIR }}/resources/icon.icns" \ + --window-pos 200 120 \ + --window-size 800 400 \ + --icon-size 100 \ + --icon "${{ env.PROJECT_NAME }}.app" 200 190 \ + --hide-extension "${{ env.PROJECT_NAME }}.app" \ + --app-drop-link 600 185 \ + "${{ env.PROJECT_NAME }}_${{ env.VER }}-universal.dmg" \ + "${{ env.DIST_DIR }}/dmg/" + ls -al ${{ env.DIST_DIR }} + + - name: Deploy Zip + uses: actions/upload-artifact@v3 + with: + name: ${{ env.PROJECT_NAME }}_${{ env.VER }}-${{ matrix.os }}-zip + path: | + ${{ env.PROJECT_NAME }}_${{ env.VER }}-macos-universal.zip + + - name: Deploy DMG + uses: actions/upload-artifact@v3 + with: + name: ${{ env.PROJECT_NAME }}_${{ env.VER }}-${{ matrix.os}}-dmg + path: | + ${{ env.PROJECT_NAME }}_${{ env.VER }}-universal.dmg + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + generate_release_notes: true + files: | + ${{ env.PROJECT_NAME }}_${{ env.VER }}-universal.dmg + ${{ env.PROJECT_NAME }}_${{ env.VER }}-macos-universal.zip diff --git a/.gitignore b/.gitignore index b6e4761..99efc91 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,4 @@ dmypy.json # Pyre type checker .pyre/ +.DS_Store diff --git a/buildPy2App.py b/buildPy2App.py index 0cb9291..dda1feb 100644 --- a/buildPy2App.py +++ b/buildPy2App.py @@ -1,35 +1,36 @@ from setuptools import setup from glob import glob from datetime import datetime +from package_alias import package -module_name = "project" -name = "Project" -version = "" -main_file = "main.py" +info = package.info -# Overrides the above -exec(open("info.py").read()) +name = info.name +version = info.version +bundle_identifier = info.bundle_identifier +module_name = info.module_name - -APP = [f'{module_name}/{main_file}'] +APP = [f'pyinstaller.py'] DATA_FILES = [ - ('resources', glob(module_name + '/resources/*.png') + glob(module_name + '/resources/*.rtf') + glob(module_name + '/resources/*.txt')), + ('resources', glob(module_name + '/resources/*')), ] OPTIONS = { + 'arch': 'universal2', + 'optimize': 2, 'iconfile': module_name + '/resources/icon.icns', - 'extra_scripts': 'info.py', - 'includes': {'PySide6.QtCore', 'PySide6.QtUiTools', 'PySide6.QtGui', 'PySide6.QtWidgets', 'certifi', 'cffi', 'pem'}, - 'excludes': {'tkinter'}, + 'includes': {'PySide6.QtCore', 'PySide6.QtUiTools', 'PySide6.QtGui', 'PySide6.QtWidgets', 'certifi', }, + 'excludes': {'tkinter', "unittest"}, 'qt_plugins': [ 'platforms/libqcocoa.dylib', 'platforms/libqminimal.dylib', 'platforms/libqoffscreen.dylib', 'styles/libqmacstyle.dylib' ], + 'argv_emulation': True, 'plist': { 'CFBundleName': name, 'CFBundleShortVersionString': version, - 'CFBundleIdentifier': f'pl.{module_name}.{name}', + 'CFBundleIdentifier': bundle_identifier, 'LSMinimumSystemVersion': '10.12.0', 'NSHumanReadableCopyright': f'Copyright © {datetime.now().year} {name} All Rights Reserved', } diff --git a/ci/appdatatemplate.xml b/ci/appdatatemplate.xml new file mode 100644 index 0000000..321f32e --- /dev/null +++ b/ci/appdatatemplate.xml @@ -0,0 +1,21 @@ + + + $BUNDLE_ID + $METADATA_LICENSE + $PROJECT_LICENSE + $PROJ_NAME + $SUMMARY + +

$DESCRIPTION

+
+ $BUNDLE_ID.desktop + $HOMEPAGE + + + $SCREENSHOT + + + + $BUNDLE_ID.desktop + +
diff --git a/ci/bintemplate b/ci/bintemplate new file mode 100644 index 0000000..fb762e8 --- /dev/null +++ b/ci/bintemplate @@ -0,0 +1,3 @@ +#!/bin/bash + +/usr/bin/$PROJ_MOD-bin/$PROJ_MOD "$@" diff --git a/ci/cleandist.py b/ci/cleandist.py new file mode 100644 index 0000000..0c03248 --- /dev/null +++ b/ci/cleandist.py @@ -0,0 +1,122 @@ +import sys +import os +import platform +import shutil + +from glob import glob + +pyver = platform.python_version_tuple()[0] + '.' + platform.python_version_tuple()[1] + +# Clean resources +def clean(glob_path: str = "", to_be_kept=None, to_be_deleted=None): + to_be_kept = to_be_kept or [] + to_be_deleted = to_be_deleted or [] + + if to_be_kept and glob_path: + for f in glob(glob_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) + + +def clean_resources(app_path: str): + PATH = '{app_path}/Contents/Resources/' + + to_be_kept = [] + to_be_deleted = [] + + clean(f'{PATH}/qt*', to_be_deleted=to_be_deleted, to_be_kept=to_be_kept) + + +def clean_pyside6(app_path: str): + # Clean PySide6 folder + + PATH = f'{app_path}/Contents/Resources/lib/python{pyver}/PySide6' + + shutil.rmtree(f'{PATH}/examples', ignore_errors=True) + shutil.rmtree(f'{PATH}/include', ignore_errors=True) + shutil.rmtree(f'{PATH}/Qt/libexec', ignore_errors=True) + + 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', + f'{PATH}/qmlformat', + f'{PATH}/qmlls', + ]) + clean(to_be_deleted=to_be_deleted) + + +def clean_qt(app_path: str): + # Clean PySide6/Qt folder + + PATH = f'{app_path}/Contents/Resources/lib/python{pyver}/PySide6/Qt' + + to_be_deleted = [f'{PATH}/qml', f'{PATH}/translations'] + + clean(to_be_deleted=to_be_deleted) + + +def clean_lib(app_path: str): + # Clean PySide6/Qt/lib folder + + PATH = f'{app_path}/Contents/Resources/lib/python{pyver}/PySide6/Qt/lib' + + to_be_kept = ['QtCore', 'QtDBus', 'QtGui', 'QtWidgets'] + to_be_deleted = [f'{PATH}/metatypes'] + + clean(f'{PATH}/Qt*', to_be_kept=to_be_kept, to_be_deleted=to_be_deleted) + + +def clean_plugins(app_path: str): + # Clean PySide6/Qt/plugins folder + PATH = f'{app_path}/Contents/Resources/lib/python{pyver}/PySide6/Qt/plugins' + + to_be_kept = ['platforms', 'styles'] + to_be_deleted = [] + + clean(f'{PATH}/*', to_be_kept=to_be_kept, to_be_deleted=to_be_deleted) + + +def symlink_shiboken(app_path: str): + # symlink .so from shiboken6 to PySide6 folder + cwd = os.getcwd() + + FROM = f'{app_path}/Contents/Resources/lib/python{pyver}/shiboken6' + TO = f'{app_path}/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) + + +def main(): + app_path = sys.argv[1] + + clean_resources(app_path) + clean_pyside6(app_path) + clean_qt(app_path) + clean_lib(app_path) + clean_plugins(app_path) + symlink_shiboken(app_path) + +main() diff --git a/ci/codesign.py b/ci/codesign.py new file mode 100644 index 0000000..0da4c78 --- /dev/null +++ b/ci/codesign.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +import os +import shutil +import sys +from pathlib import Path +from typing import Generator, List, Optional + +from macholib.MachO import MachO + +import contextlib +import fcntl +import os +import subprocess +import sys +import time + +from macholib.util import is_platform_file + +def _dosign(*path): + with reset_blocking_status(): + subprocess.check_call( + ( + "codesign", + "--deep", + "--force", + "-s", + "-", + "--preserve-metadata=identifier,entitlements,flags,runtime", + "-f", + "-vvvv", + ) + + path, + ) + +def _macho_find(path): + for basename, _dirs, files in os.walk(path): + for fn in files: + path = os.path.join(basename, fn) + if is_platform_file(path): + yield path + +def codesign_adhoc(bundle): + """ + (Re)sign a bundle + + Signing should be done "depth-first", sign + libraries before signing the libraries/executables + linking to them. + + The current implementation is a crude hack, + but is better than nothing. Signing properly requires + performing a topological sort using dependencies. + + "codesign" will resign the entire bundle, but only + if partial signatures are valid. + """ + # try: + # _dosign(bundle) + # return + # except subprocess.CalledProcessError: + # pass + + platfiles = list(_macho_find(bundle)) + print("sign", platfiles) + while platfiles: + failed = [] + for file in platfiles: + failed = [] + try: + _dosign(file) + except subprocess.CalledProcessError: + failed.append(file) + if failed == platfiles: + raise RuntimeError("Cannot sign bundle %r" % (bundle,)) + platfiles = failed + + for _ in range(5): + try: + _dosign(bundle) + break + except subprocess.CalledProcessError: + time.sleep(1) + continue + + +@contextlib.contextmanager +def reset_blocking_status(): + """ + Contextmanager that resets the non-blocking status of + the std* streams as necessary. Used with all calls of + xcode tools, mostly because ibtool tends to set the + std* streams to non-blocking. + """ + orig_nonblocking = [ + fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_NONBLOCK for fd in (0, 1, 2) + ] + + try: + yield + + finally: + for fd, is_nonblocking in zip((0, 1, 2), orig_nonblocking): + cur = fcntl.fcntl(fd, fcntl.F_GETFL) + if is_nonblocking: + reset = cur | os.O_NONBLOCK + else: + reset = cur & ~os.O_NONBLOCK + + if cur != reset: + print("Resetting blocking status of %s" % (fd,)) + fcntl.fcntl(fd, fcntl.F_SETFL, reset) + +def create_symlink(folder: Path) -> None: + """Create the appropriate symlink in the MacOS folder + pointing to the Resources folder. + """ + sibbling = Path(str(folder).replace("MacOS", "")) + + # PyQt5/Qt/qml/QtQml/Models.2 + root = str(sibbling).partition("Contents")[2].lstrip("/") + # ../../../../ + backward = "../" * (root.count("/") + 1) + # ../../../../Resources/PyQt5/Qt/qml/QtQml/Models.2 + good_path = f"{backward}Resources/{root}" + + folder.symlink_to(good_path) + + +def fix_dll(dll: Path) -> None: + """Fix the DLL lookup paths to use relative ones for Qt dependencies. + Inspiration: PyInstaller/depend/dylib.py:mac_set_relative_dylib_deps() + Currently one header is pointing to (we are in the Resources folder): + @loader_path/../../../../QtCore (it is referencing to the old MacOS folder) + It will be converted to: + @loader_path/../../../../../../MacOS/QtCore + """ + + def match_func(pth: str) -> Optional[str]: + """Callback function for MachO.rewriteLoadCommands() that is + called on every lookup path setted in the DLL headers. + By returning None for system libraries, it changes nothing. + Else we return a relative path pointing to the good file + in the MacOS folder. + """ + basename = os.path.basename(pth) + if not basename.startswith("Qt"): + return None + return f"@loader_path{good_path}/{basename}" + + # Resources/PyQt5/Qt/qml/QtQuick/Controls.2/Fusion + root = str(dll.parent).partition("Contents")[2][1:] + # /../../../../../../.. + backward = "/.." * (root.count("/") + 1) + # /../../../../../../../MacOS + good_path = f"{backward}/MacOS" + + # Rewrite Mach headers with corrected @loader_path + mach_dll = MachO(dll) + mach_dll.rewriteLoadCommands(match_func) + with open(mach_dll.filename, "rb+") as f: + for header in mach_dll.headers: + f.seek(0) + mach_dll.write(f) + f.seek(0, 2) + f.flush() + + +def find_problematic_folders(folder: Path) -> Generator[Path, None, None]: + """Recursively yields problematic folders (containing a dot in their name).""" + for path in folder.iterdir(): + print(path) + if not path.is_dir() or path.is_symlink(): + # Skip simlinks as they are allowed (even with a dot) + continue + if "." in path.name: + yield path + else: + yield from find_problematic_folders(path) + + +def move_contents_to_resources(folder: Path) -> Generator[Path, None, None]: + """Recursively move any non symlink file from a problematic folder + to the sibbling one in Resources. + """ + for path in folder.iterdir(): + if path.is_symlink(): + continue + if path.name == "qml": + yield from move_contents_to_resources(path) + else: + sibbling = Path(str(path).replace("MacOS", "Resources")) + sibbling.parent.mkdir(parents=True, exist_ok=True) + shutil.move(path, sibbling) + yield sibbling + + +def main(args: List[str]): + """ + Fix the application to allow codesign (NXDRIVE-1301). + Take one or more .app as arguments: "Nuxeo Drive.app". + To overall process will: + - move problematic folders from MacOS to Resources + - fix the DLLs lookup paths + - create the appropriate symbolic link + """ + for app in args: + name = os.path.basename(app) + print(f">>> [{name}] Fixing Qt folder names") + path = Path(app) / "Contents" / "MacOS" + for folder in find_problematic_folders(path): + for file in move_contents_to_resources(folder): + try: + fix_dll(file) + except (ValueError, IsADirectoryError): + continue + shutil.rmtree(folder) + create_symlink(folder) + print(f" !! Fixed {folder}") + codesign_adhoc(app) + print(f">>> [{name}] Application fixed.") + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/ci/deb-script.sh b/ci/deb-script.sh new file mode 100755 index 0000000..2eb1a3b --- /dev/null +++ b/ci/deb-script.sh @@ -0,0 +1,52 @@ +#! /bin/bash + +set -x +set -e + +BUILD_DIR=".debpkg" +mkdir -p "$BUILD_DIR" + +# store repo root as variable +REPO_ROOT=$(readlink -f $(dirname $(dirname "$0"))) +OLD_CWD=$(readlink -f .) +FILE_LOC=$(find $REPO_ROOT -name info.py) + +export VER="$(cat $FILE_LOC | grep -w version | awk -F'"' '$0=$2')" +export PROJ_NAME="$(cat $FILE_LOC | grep -w name | awk -F'"' '$0=$2')" +export PROJ_MOD="$(cat $FILE_LOC | grep -w module_name | awk -F'"' '$0=$2')" +export MAIN_FILE="$(cat $FILE_LOC | grep -w main_file | awk -F'"' '$0=$2')" +export BUNDLE_ID="$(cat $FILE_LOC | grep -w bundle_identifier | awk -F'"' '$0=$2')" +export PROJECT_LICENSE="$(cat $FILE_LOC | grep -w project_license | awk -F'"' '$0=$2')" +export SCREENSHOT="$(cat $FILE_LOC | grep -w screenshot | awk -F'"' '$0=$2')" +export METADATA_LICENSE="$(cat $FILE_LOC | grep -w metadata_license | awk -F'"' '$0=$2')" +export DESCRIPTION="$(cat $FILE_LOC | grep -w description | awk -F'"' '$0=$2')" +export HOMEPAGE="$(cat $FILE_LOC | grep -w homepage | awk -F'"' '$0=$2')" +export SUMMARY="$(cat $FILE_LOC | grep -w summary | awk -F'"' '$0=$2')" +export CATEGORIES="$(cat $FILE_LOC | grep -w categories | awk -F'"' '$0=$2')" +export MIMETYPE="$(cat $FILE_LOC | grep -w mimetype | awk -F'"' '$0=$2')" +export KEYWORDS="$(cat $FILE_LOC | grep -w keywords | awk -F'"' '$0=$2')" +export APP_TYPE="$(cat $FILE_LOC | grep -w application_type | awk -F'"' '$0=$2')" +export MAIN_MOD="$(cat $FILE_LOC | grep -w main_module | awk -F'"' '$0=$2')" +export EMAIL="$(cat $FILE_LOC | grep -w email | awk -F'"' '$0=$2')" + +pushd "$BUILD_DIR" + +# move and rename .desktop file +cat > $PROJ_MOD.desktop <(envsubst < $REPO_ROOT/ci/projectemplate.desktop) + + +mkdir -p $PROJ_MOD/usr/bin/$PROJ_MOD-bin + +chmod +x $REPO_ROOT/$PROJ_MOD-tar/$PROJ_MOD +cp -r $REPO_ROOT/$PROJ_MOD-tar/* $PROJ_MOD/usr/bin/$PROJ_MOD-bin/ + +cat > $PROJ_MOD/usr/bin/$PROJ_MOD <(envsubst < $REPO_ROOT/ci/bintemplate) +chmod +x $PROJ_MOD/usr/bin/$PROJ_MOD + +mkdir -p $PROJ_MOD/usr/share/applications +mkdir -p $PROJ_MOD/usr/lib/$PROJ_MOD + +cp $PROJ_MOD.desktop $PROJ_MOD/usr/share/applications +chmod +x $PROJ_MOD/usr/share/applications/$PROJ_MOD.desktop + +mkdir -p $PROJ_MOD/DEBIAN diff --git a/ci/projectemplate.desktop b/ci/projectemplate.desktop new file mode 100644 index 0000000..ae248e1 --- /dev/null +++ b/ci/projectemplate.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=$PROJ_NAME +Comment=$DESCRIPTION +Exec=$PROJ_MOD %u +Type=$APP_TYPE +Icon=icon +Categories=$CATEGORIES; +MimeType=$MIME_TYPES +Keywords=$PROJ_MOD;$KEYWORDS diff --git a/create_pyinstaller_file.py b/create_pyinstaller_file.py new file mode 100644 index 0000000..0d671e0 --- /dev/null +++ b/create_pyinstaller_file.py @@ -0,0 +1,7 @@ +from package_alias import package_name, package + +with open("pyinstaller.py", "w+") as f: + f.writelines([ + f"from {package_name} import {package.info.main_module}\n", + f"{package.info.main_module}.main()" + ]) diff --git a/info.cfg b/info.cfg deleted file mode 100644 index cd1ca59..0000000 --- a/info.cfg +++ /dev/null @@ -1,3 +0,0 @@ -version="0.1.0" -name="Project" -module_name="project" diff --git a/info.py b/info.py deleted file mode 100644 index 29f6aee..0000000 --- a/info.py +++ /dev/null @@ -1,6 +0,0 @@ -version="0.1.0" -name="Project" -module_name="project" -company_name="Project Company" -bundle_identifier="com.projectcompany.project.Project" -main_file="main.py" diff --git a/package_alias.py b/package_alias.py new file mode 100644 index 0000000..a1e42a9 --- /dev/null +++ b/package_alias.py @@ -0,0 +1,11 @@ +import sys +import os +from glob import glob + +info_name = 'info' +info_file = glob(os.path.join('*', f'{info_name}.py'))[0] +package_name = info_file.split(os.path.sep)[0] + +package = __import__(f"{package_name}", fromlist=["main", "info"]) +if package_name not in sys.modules: + sys.modules[package_name] = package diff --git a/project/__init__.py b/project/__init__.py index 8e4bbd7..e69de29 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -1,5 +0,0 @@ -from .. import info - -version = info.version -name = info.name -module_name = info.module_name diff --git a/project/info.py b/project/info.py new file mode 100644 index 0000000..3b67cda --- /dev/null +++ b/project/info.py @@ -0,0 +1,18 @@ +version="0.1.0" +name="Project" +module_name="project" +company_name="Project Company" +bundle_identifier="com.projectcompany.project.Project" +main_file="main.py" +main_module="main" +metadata_license="MIT" +project_license="PRIVATE" +email="user@example.com" +homepage="https://example.com" +screenshot="" +summary="A project of your creation." +description="A project of your creation." +keywords="interesting;good" +categories="CategoryA;CategoryB" +mimetype="image/x-foo;image/y-foo" +application_type="Application" diff --git a/project/main.py b/project/main.py index 30fc485..1706b3b 100644 --- a/project/main.py +++ b/project/main.py @@ -1,6 +1,7 @@ from PySide6.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget from PySide6.QtGui import QIcon -from . import name as project_name +from . import info +project_name = info.name import sys @@ -22,7 +23,10 @@ class MainWindow(QMainWindow): self.show() -if __name__ == '__main__': +def main(): app = QApplication(sys.argv) w = MainWindow() app.exec_() + +if __name__ == '__main__': + main() diff --git a/project/resources/icon.ico b/project/resources/icon.ico new file mode 100644 index 0000000..f62a82e Binary files /dev/null and b/project/resources/icon.ico differ diff --git a/project/resources/icon.png b/project/resources/icon.png new file mode 100644 index 0000000..22bfcae Binary files /dev/null and b/project/resources/icon.png differ