Add github actions for macos #1

Merged
jyapayne merged 30 commits from create-macos-action into master 2022-11-26 04:25:32 +00:00
18 changed files with 850 additions and 29 deletions

362
.github/workflows/default.yml vendored Normal file
View file

@ -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

1
.gitignore vendored
View file

@ -127,3 +127,4 @@ dmypy.json
# Pyre type checker
.pyre/
.DS_Store

View file

@ -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',
}

21
ci/appdatatemplate.xml Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>$BUNDLE_ID</id>
<metadata_license>$METADATA_LICENSE</metadata_license>
<project_license>$PROJECT_LICENSE</project_license>
<name>$PROJ_NAME</name>
<summary>$SUMMARY</summary>
<description>
<p>$DESCRIPTION</p>
</description>
<launchable type="desktop-id">$BUNDLE_ID.desktop</launchable>
<url type="homepage">$HOMEPAGE</url>
<screenshots>
<screenshot type="default">
<image>$SCREENSHOT</image>
</screenshot>
</screenshots>
<provides>
<id>$BUNDLE_ID.desktop</id>
</provides>
</component>

3
ci/bintemplate Normal file
View file

@ -0,0 +1,3 @@
#!/bin/bash
/usr/bin/$PROJ_MOD-bin/$PROJ_MOD "$@"

122
ci/cleandist.py Normal file
View file

@ -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()

224
ci/codesign.py Normal file
View file

@ -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:]))

52
ci/deb-script.sh Executable file
View file

@ -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

View file

@ -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

View file

@ -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()"
])

View file

@ -1,3 +0,0 @@
version="0.1.0"
name="Project"
module_name="project"

View file

@ -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"

11
package_alias.py Normal file
View file

@ -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

View file

@ -1,5 +0,0 @@
from .. import info
version = info.version
name = info.name
module_name = info.module_name

18
project/info.py Normal file
View file

@ -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"

View file

@ -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()

BIN
project/resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
project/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB