Last active
September 26, 2020 17:07
-
-
Save fizzyade/a79d84bb23e1d0e30defa53d2336c5d8 to your computer and use it in GitHub Desktop.
Python script to automate deployment of a Qt based application as a macOS DMG, Windows Installer and Linux AppImage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# Copyright (C) 2019 Adrian Carpenter | |
# | |
# Multiplatform Qt Deployment Tool | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |
# | |
import platform | |
import os | |
import fnmatch | |
import shutil | |
import glob | |
import tempfile | |
import argparse | |
import re | |
import time | |
import subprocess | |
import sys | |
import logging | |
import datetime | |
import enum | |
deploymentProject = "Regular Expressions 101" | |
if platform.python_version_tuple()<('3','6','0'): | |
print('requires python >= 3.6') | |
quit(1) | |
try: | |
from colorama import Fore, Back, Style, init | |
init(autoreset=True) | |
except ModuleNotFoundError: | |
class Style: | |
BRIGHT = '' | |
RESET = '' | |
class Fore: | |
GREEN = '' | |
RED = '' | |
CYAN = '' | |
RESET = '' | |
def timeDelta(seconds): | |
seconds = abs(int(seconds)) | |
days, seconds = divmod(seconds, 86400) | |
hours, seconds = divmod(seconds, 3600) | |
minutes, seconds = divmod(seconds, 60) | |
if days > 0: | |
return(f'{days}d{hours}h{minutes}m{seconds}s') | |
elif hours > 0: | |
return(f'{hours}h{minutes}m{seconds}s') | |
elif minutes > 0: | |
return(f'{minutes}m{seconds}s') | |
else: | |
return(f'{seconds}s') | |
def startMessage(message): | |
sys.stdout.write(Style.BRIGHT+f'> {message} ') | |
def endMessage(state, message=None): | |
if state: | |
if platform.system()=="Windows": | |
sys.stdout.write(Style.BRIGHT+'['+Fore.GREEN+'Y'+Fore.RESET+']\r\n') | |
else: | |
sys.stdout.write(Style.BRIGHT+'['+Fore.GREEN+'✓'+Fore.RESET+']\r\n') | |
else: | |
if platform.system()=="Windows": | |
sys.stdout.write(Style.BRIGHT+'['+Fore.RED+'N'+Fore.RESET+']\r\n') | |
else: | |
sys.stdout.write(Style.BRIGHT+'['+Fore.RED+'✘'+Fore.RESET+']\r\n') | |
if not state and message: | |
print('\r\n'+Fore.RED+'ERROR: '+Fore.RESET+message) | |
def parent(path): | |
return(os.path.normpath(os.path.join(path, os.pardir))) | |
def execute(command): | |
output = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
return(output.returncode, output.stdout.decode('utf-8')) | |
def which(appname): | |
if platform.system()=="Windows" : | |
command = 'where' | |
else: | |
command = 'which' | |
status, output = execute(f'{command} {appname}') | |
if status and output: | |
return(output.split()[0]) | |
def run(command) : | |
stream = os.popen(command) | |
return stream.read() | |
def macSignBinary(file, cert): | |
return(execute(f'codesign --verify --timestamp -o runtime --deep --force --sign "{cert}" "{file}"')) | |
def winSignBinary(signtool, file, cert, timeserver): | |
return(execute(f'{signtool} sign /n "{cert}" /t {timeserver} /fd sha256 /v "{file}"')) | |
def notarizeFile(file, username, password): | |
uuidPattern = re.compile(r'RequestUUID\s=\s(?P<requestUUID>[a-f|0-9]{8}-[a-f|0-9]{4}-[a-f|0-9]{4}-[a-f|0-9]{4}-[a-f|0-9]{12})\n') | |
statusPattern = re.compile(r'\s*Status:\s(?P<status>\w+)\n') | |
filename = os.path.basename(file) | |
uploadId = str(int(time.time())) | |
resultCode,result = execute(f'xcrun altool --notarize-app --primary-bundle-id "com.fizzyade.{uploadId}" -u {args.appleid} --password {args.password} --file "{file}"') | |
if resultCode: | |
return(False) | |
match = uuidPattern.search(result) | |
if not match: | |
return(False) | |
requestId = match.groupdict()["requestUUID"] | |
if not requestId: | |
return(False) | |
while True: | |
resultCode,result = execute(f'xcrun altool --notarization-info {requestId} -u {username} --password {password}') | |
if not resultCode: | |
match = statusPattern.search(result) | |
if match: | |
status = match.groupdict()["status"] | |
if status=="in progress": | |
time.sleep(10) | |
continue | |
if status=="success": | |
break | |
if status=="invalid": | |
break | |
return(status) | |
# application entry point | |
hostArch = "x86" | |
parser = argparse.ArgumentParser(description='Qt Deployment Tool') | |
parser.add_argument('--qtdir', type=str, nargs='?', help='path to qt') | |
parser.add_argument('--curlbin', type=str, nargs='?', help='path to curl binary') | |
if platform.system()=="Darwin": | |
status, hostArch = execute(f'arch') | |
if not status: | |
hostArch = hostArch.strip() | |
parser.add_argument('--arch', choices=['x86_64', 'arm64', 'universal'], type=str, default='x64_64', nargs='?', help='architecture type to deploy') | |
else: | |
parser.add_argument('--arch', choices=['x86', 'x86_64'], type=str, default='x86_64', nargs='?', help='architecture type to deploy') | |
parser.add_argument('--type', choices=['release', 'debug'], default='release', type=str, nargs='?', help='type of build to deploy') | |
parser.add_argument('--cert', type=str, nargs='?', help='certificate id to sign with') | |
if platform.system()=="Linux": | |
status, hostArch = execute(f'arch') | |
if not status: | |
hostArch = hostArch.strip() | |
parser.add_argument('--linuxdeployqt', type=str, default=f'tools/linuxdeployqt/linuxdeployqt-6-{hostArch}.AppImage', nargs='?', help='path to linuxdeployqt') | |
parser.add_argument('--appimagetool', type=str, default=f'tools/appimagetool/appimagetool-{hostArch}.AppImage', nargs='?', help='path to appimagetool') | |
if platform.system()=="Windows": | |
parser.add_argument('--timeserver', type=str, default='http://time.certum.pl/', nargs='?', help='time server to use for signing') | |
parser.add_argument('--signtool', type=str, nargs='?', default='tools\\smartcardtools\\x64\\ScSignTool.exe', help='path to signing binary') | |
if platform.system()=="Darwin": | |
parser.add_argument('--appleid', type=str, nargs='?', help='apple id to use for notarization') | |
parser.add_argument('--password', type=str, nargs='?', help='password for apple id') | |
args = parser.parse_args() | |
buildArch = args.arch | |
buildType = args.type.capitalize() | |
if platform.system()=="Windows": | |
print(Style.BRIGHT+'Deployment process started at '+str(datetime.datetime.now())+'\r\n') | |
startTime = time.time() | |
isRDP = os.environ['SESSIONNAME'].startswith('RDP-') | |
# check curl is available | |
startMessage('Checking for curl...') | |
if args.curlbin: | |
if os.path.isfile(args.curlbin): | |
curl = args.curlbin | |
else: | |
curl = None | |
else: | |
curl = which('curl.exe') | |
if not curl: | |
endMessage(False, 'curl could not be found. (see --curlbin).') | |
exit(1) | |
endMessage(True) | |
# check for qt installation | |
startMessage('Checking qtdir...') | |
if args.qtdir: | |
if os.path.isfile(f'{args.qtdir}\\bin\\windeployqt.exe'): | |
windeployqt = f'{args.qtdir}\\bin\\windeployqt.exe' | |
else: | |
windeployqt = None | |
else: | |
windeployqt = which('windeployqt') | |
if not windeployqt: | |
endMessage(False, 'qt could not be found. (see --qtdir).') | |
exit(1) | |
endMessage(True) | |
# create tools folder if it doesn't exist | |
startMessage('Setting up tools directory...') | |
if not os.path.exists(f'tools'): | |
os.makedirs(f'tools') | |
endMessage(True) | |
tempdir = os.path.normpath(tempfile.mkdtemp()) | |
signtool = args.signtool | |
if args.cert: | |
if not os.path.exists(signtool): | |
startMessage('Downloading SmartCardTools...') | |
resultCode, resultOutput = execute(f'cd \"{tempdir}\" && \"{curl}\" -LJO https://www.mgtek.com/files/smartcardtools.zip') | |
if resultCode: | |
endMessage(False, f'unable to download SmartCardTools.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
resultCode, resultOutput = execute(f'cd \"{tempdir}\" && \"{curl}\" -LJO ftp://ftp.info-zip.org/pub/infozip/win32/unz600xn.exe & unz600xn -jo unzip.exe') | |
if resultCode: | |
endMessage(False, f'unable to download info-zip tools.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
resultCode, resultOutput = execute(f'\"{tempdir}\\unzip\" \"{tempdir}\\smartcardtools.zip\" -d tools\\smartcardtools') | |
if resultCode: | |
endMessage(False, f'unable to unzip SmartCardTools.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
signtool = 'tools\\smartcardtools\\x64\\ScSignTool.exe' | |
endMessage(True) | |
# remove previous deployment files and copy current binaries | |
startMessage('Setting up deployment directory...') | |
deployDir = f'bin\\{buildArch}\\Deploy' | |
binaryDir = f'bin\\{buildArch}\\{buildType}' | |
extensions=['.exe', '.dll'] | |
if os.path.exists(deployDir): | |
shutil.rmtree(deployDir) | |
signList = [] | |
os.makedirs(deployDir) | |
for file in glob.glob(f'{binaryDir}\\**\\*', recursive=True): | |
basename, extension = os.path.splitext(file) | |
if os.path.isdir(file): | |
os.makedirs(file.replace(binaryDir, deployDir, 1)) | |
if extension in extensions: | |
destFile = file.replace(binaryDir, deployDir, 1) | |
shutil.copy2(file, destFile) | |
signList.append(destFile) | |
files = [] | |
for extension in extensions: | |
files += glob.glob(f'{deployDir}\\*{extension}') | |
if not files: | |
endMessage(False, 'no files could be found to deploy.') | |
exit(1) | |
endMessage(True) | |
# sign the application binaries | |
if args.cert: | |
startMessage('Signing binaries...') | |
if isRDP: | |
endMessage(False, f'Unable to sign during an RDP session.') | |
exit(1) | |
for file in signList: | |
resultCode, resultOutput = winSignBinary(signtool, file, args.cert, args.timeserver) | |
if resultCode: | |
endMessage(False, f'there was a problem signing a file ({file}).\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
filesString = '' | |
for file in files: | |
filesString += f'{file} ' | |
filesString = filesString.strip() | |
# run windeplotqt | |
startMessage('Running windeployqt...') | |
resultCode, resultOutput = execute(f'{windeployqt} --dir "{deployDir}" "{filesString}"') | |
if resultCode: | |
endMessage(False, f'there was a problem running windeployqt.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
# run advanced installer | |
startMessage('Creating installer...') | |
resultCode, resultOutput = execute(f'AdvancedInstaller.com /build "installer\\{deploymentProject}.aip"') | |
if resultCode: | |
endMessage(False, f'there was a problem creating the installer.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
# sign the installer file | |
if args.cert: | |
startMessage('Signing installer...') | |
if isRDP: | |
endMessage(False, f'Unable to sign during an RDP session.') | |
exit(1) | |
resultCode, resultOutput = winSignBinary(signtool, f'deployment\\{deploymentProject}.exe', args.cert, args.timeserver) | |
if resultCode: | |
endMessage(False, f'there was a problem signing the installer.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
endTime = time.time() | |
# done! | |
print(f'\r\n'+Style.BRIGHT+Fore.CYAN+f'Finished! Installer at "deployment\\{deploymentProject}.exe" is '+Fore.GREEN+'ready'+Fore.CYAN+' for distribution.') | |
print(Style.BRIGHT+f'\r\nTotal time taken to perform deployment was '+timeDelta(endTime-startTime)+'.') | |
exit(0) | |
if platform.system()=="Linux" : | |
print(Style.BRIGHT+'Deployment process started at '+str(datetime.datetime.now())+'\r\n') | |
startTime = time.time() | |
# check curl is available | |
startMessage('Checking for curl...') | |
if args.curlbin: | |
if os.path.isfile(args.curlbin): | |
curl = args.curlbin | |
else: | |
curl = None | |
else: | |
curl = which('curl') | |
if not curl: | |
endMessage(False, 'curl could not be found. (see --curlbin).') | |
exit(1) | |
endMessage(True) | |
# check for qt installation | |
startMessage('Checking qtdir...') | |
if args.qtdir: | |
if os.path.isfile(args.qtdir+'/bin/qmake'): | |
qtdir = args.qtdir | |
else: | |
qtdir = None | |
else: | |
qmake = which('qmake') | |
if qmake: | |
qtdir = parent(os.path.normpath(os.path.dirname(qmake))) | |
else: | |
qtdir = None | |
if not qtdir: | |
endMessage(False, 'qt directory could not be found. (see --qtdir).') | |
exit(1) | |
else: | |
if not os.path.isdir(qtdir): | |
endMessage(False, 'qt directory could not be found. (see --qtdir).') | |
exit(1) | |
endMessage(True) | |
# create tools folder if it doesn't exist | |
startMessage('Setting up tools directory...') | |
if not os.path.exists(f'tools'): | |
os.makedirs(f'tools') | |
endMessage(True) | |
# download linuxdeployqt | |
linuxdeployqt = args.linuxdeployqt | |
if not os.path.isfile(linuxdeployqt) : | |
startMessage('Downloading linuxdeployqt...') | |
if not os.path.exists('tools/linuxdeployqt'): | |
os.mkdir('tools/linuxdeployqt') | |
resultCode, resultOutput = execute(f'cd tools/linuxdeployqt; curl -LJO https://github.com/probonopd/linuxdeployqt/releases/download/6/linuxdeployqt-6-{hostArch}.AppImage') | |
if resultCode: | |
endMessage(False, f'unable to download linuxdeployqt.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
resultCode, resultOutput = execute(f'chmod +x tools/linuxdeployqt/linuxdeployqt-6-{hostArch}.AppImage') | |
if resultCode: | |
endMessage(False, f'unable to set permissions on linuxdeployqt.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
linuxdeployqt = f'tools/linuxdeployqt/linuxdeployqt-6-{hostArch}.AppImage' | |
endMessage(True) | |
appimagetool = args.appimagetool | |
if not os.path.isfile(appimagetool): | |
startMessage('Downloading appimagetool...') | |
if not os.path.exists('tools/appimagetool'): | |
os.mkdir('tools/appimagetool') | |
resultCode, resultOutput = execute(f'cd tools/appimagetool; curl -LJO https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-{hostArch}.AppImage') | |
if resultCode: | |
endMessage(False, f'unable to download appimagetool.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
resultCode, resultOutput = execute(f'chmod +x tools/appimagetool/appimagetool-{hostArch}.AppImage') | |
if resultCode: | |
endMessage(False, f'unable to set permissions on appimagetool.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
appimagetool = f'tools/appimagetool/appimagetool-{hostArch}.AppImage' | |
endMessage(True) | |
# remove previous deployment files and copy current binaries | |
startMessage('Setting up deployment directory...') | |
if os.path.exists(f'bin/{buildArch}/Deploy/'): | |
shutil.rmtree(f'bin/{buildArch}/Deploy/') | |
if os.path.exists(f'deployment'): | |
shutil.rmtree(f'deployment') | |
os.makedirs(f'deployment') | |
os.makedirs(f'bin/{buildArch}/Deploy/usr/bin') | |
os.makedirs(f'bin/{buildArch}/Deploy/usr/lib') | |
os.makedirs(f'bin/{buildArch}/Deploy/usr/share') | |
os.makedirs(f'bin/{buildArch}/Deploy/usr/share/applications') | |
shutil.copy2(f'bin/{buildArch}/{buildType}/{deploymentProject}', f'bin/{buildArch}/Deploy/usr/bin') | |
shutil.copy2(f'installer/{deploymentProject}.png', f'bin/{buildArch}/Deploy/regex101.png') | |
shutil.copy2(f'installer/{deploymentProject}.desktop', f'bin/{buildArch}/Deploy/{deploymentProject}.desktop') | |
shutil.copy2(f'installer/AppRun', f'bin/{buildArch}/Deploy/') | |
for file in glob.glob(f'bin/{buildArch}/{buildType}/*.so') : | |
shutil.copy2(file, f'bin/{buildArch}/Deploy/usr/lib') | |
endMessage(True) | |
# create the app dir | |
startMessage('Running linuxdeployqt...') | |
resultCode, resultOutput = execute(f'{linuxdeployqt} "bin/{buildArch}/Deploy/usr/bin/{deploymentProject}" -qmake="{qtdir}/bin/qmake" -exclude-libs="libqsqlodbc,libqsqlpsql" -unsupported-allow-new-glibc') | |
if resultCode: | |
endMessage(False, f'there was a problem running linuxdeployqt.\r\n\r\n{resultCode}\r\n') | |
exit(1) | |
endMessage(True) | |
# create the AppImage | |
signParameters = '' | |
if args.cert: | |
signParameters = f'-s --sign-key={args.cert} ' | |
startMessage('Creating AppImage...') | |
resultCode, resultOutput = execute(f'ARCH={buildArch} {appimagetool} -g {signParameters} bin/{buildArch}/Deploy "deployment/{deploymentProject}-{buildArch}.AppImage"') | |
if resultCode: | |
endMessage(False, f'there was a problem creating the AppImage.\r\n\r\n{resultCode}\r\n') | |
exit(1) | |
endMessage(True) | |
endTime = time.time() | |
# done! | |
print(f'\r\n'+Style.BRIGHT+Fore.CYAN+f'Finished! AppImage at "deployment/{deploymentProject}-{buildArch}.AppImage" is '+Fore.GREEN+'ready'+Fore.CYAN+' for distribution.') | |
print(Style.BRIGHT+f'\r\nTotal time taken to perform deployment was '+timeDelta(endTime-startTime)+'.') | |
exit(0) | |
if platform.system()=="Darwin": | |
print(Style.BRIGHT+'Deployment process started at '+str(datetime.datetime.now())+'\r\n') | |
startTime = time.time() | |
# check for qt installation | |
startMessage('Checking qtdir...') | |
if args.qtdir: | |
if os.path.isfile(args.qtdir+'/bin/qmake'): | |
qtdir = args.qtdir | |
else: | |
qtdir = None | |
else: | |
qmake = which('qmake') | |
if qmake: | |
qtdir = parent(os.path.normpath(os.path.dirname(qmake))) | |
else: | |
qtdir = None | |
if not qtdir: | |
endMessage(False, 'qt directory could not be found. (see --qtdir).') | |
exit(1) | |
else: | |
if not os.path.isdir(qtdir): | |
endMessage(False, 'qt directory could not be found. (see --qtdir).') | |
exit(1) | |
endMessage(True) | |
# remove previous deployment files and copy current binaries | |
startMessage('Setting up deployment directory...') | |
if os.path.exists(f'deployment'): | |
shutil.rmtree(f'deployment') | |
os.makedirs(f'deployment') | |
if os.path.exists(f'bin/{buildArch}/Deploy'): | |
shutil.rmtree(f'bin/{buildArch}/Deploy') | |
os.makedirs(f'bin/{buildArch}/Deploy') | |
endMessage(True) | |
# create tools folder if it doesn't exist | |
startMessage('Setting up tools directory...') | |
if not os.path.exists(f'tools'): | |
os.makedirs(f'tools') | |
endMessage(True) | |
if not os.path.isfile('tools/macdeployqtfix/macdeployqtfix.py'): | |
if os.path.exists('tools/macdeployqtfix'): | |
shutil.rmtree(f'tools/macdeployqtfix') | |
startMessage('Cloning macdeployqtfix...') | |
resultCode, resultOutput = execute('cd tools;git clone https://github.com/fizzyade/macdeployqtfix.git') | |
if resultCode: | |
endMessage(False, f'unable to clone macdeployqtfix.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
if not os.path.isfile('tools/create-dmg/create-dmg'): | |
if os.path.exists('tools/create-dmg'): | |
shutil.rmtree(f'tools/create-dmg') | |
startMessage('Cloning create-dmg...') | |
resultCode, resultOutput = execute('cd tools;git clone https://github.com/andreyvit/create-dmg.git') | |
if resultCode: | |
endMessage(False, f'unable to clone create-dmg.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
if not buildArch=="universal": | |
shutil.copytree(f'bin/{buildArch}/{buildType}/{deploymentProject}.app', f'bin/{buildArch}/Deploy/{deploymentProject}.app', symlinks=True) | |
else: | |
if not os.path.isfile('tools/makeuniversal/makeuniversal'): | |
if os.path.exists('tools/makeuniversal'): | |
shutil.rmtree(f'tools/makeuniversal') | |
startMessage('Cloning makeuniversal...') | |
resultCode, resultOutput = execute('cd tools;git clone https://github.com/fizzyade/makeuniversal.git') | |
if resultCode: | |
endMessage(False, f'unable to clone makeuniversal.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
startMessage('Building makeuniversal...') | |
resultCode, resultOutput = execute(f'cd tools/makeuniversal;{qtdir}/bin/qmake;make') | |
if resultCode: | |
endMessage(False, f'error building makeuniversal.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
startMessage('Running makeuniversal...') | |
resultCode, resultOutput = execute(f'tools/makeuniversal/makeuniversal bin/universal/Deploy/{deploymentProject}.app bin/x86_64/{buildType}/{deploymentProject}.app bin/arm64/{buildType}/{deploymentProject}.app') | |
if resultCode: | |
endMessage(False, f'error building makeuniversal.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
# run standard qt deployment tool | |
startMessage('Running macdeployqt...') | |
resultCode, resultOutput = execute(f'{qtdir}/bin/macdeployqt "bin/{buildArch}/Deploy/{deploymentProject}.app" -no-strip') | |
if resultCode: | |
endMessage(False, f'there was a problem running macdeployqt.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
# remove the sql drivers that we don't use | |
startMessage('Removing unwanted qt plugins...') | |
if os.path.isfile(f'bin/{buildArch}/Deploy/{deploymentProject}.app/Contents/PlugIns/sqldrivers/libqsqlodbc.dylib') : | |
os.remove(f'bin/{buildArch}/Deploy/{deploymentProject}.app/Contents/PlugIns/sqldrivers/libqsqlodbc.dylib') | |
if os.path.isfile(f'bin/{buildArch}/Deploy/{deploymentProject}.app/Contents/PlugIns/sqldrivers/libqsqlpsql.dylib') : | |
os.remove(f'bin/{buildArch}/Deploy/{deploymentProject}.app/Contents/PlugIns/sqldrivers/libqsqlpsql.dylib') | |
endMessage(True) | |
# run fixed qt deployment tool | |
startMessage('Running macdeployqtfix...') | |
sys.path.insert(1, 'tools/macdeployqtfix') | |
import macdeployqtfix as fixDeploy | |
fixDeploy.GlobalConfig.qtpath = os.path.normpath(f'{qtdir}/bin') | |
fixDeploy.GlobalConfig.exepath = f'bin/{buildArch}/Deploy/{deploymentProject}.app' | |
fixDeploy.GlobalConfig.logger = logging.getLogger() | |
fixDeploy.GlobalConfig.logger.addHandler(logging.NullHandler()) | |
if not fixDeploy.fix_main_binaries(): | |
endMessage(False, 'there was a problem running macdeployqtfix.') | |
exit(1) | |
endMessage(True) | |
# sign the application | |
startMessage('Signing binaries...') | |
for file in glob.glob(f'bin/{buildArch}/Deploy/{deploymentProject}.app/**/*.framework', recursive=True): | |
resultCode, resultOutput = macSignBinary(file, args.cert) | |
if resultCode: | |
endMessage(False, f'there was a problem signing a file ({file}).\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
for file in glob.glob(f'bin/{buildArch}/Deploy/{deploymentProject}.app/**/*.dylib', recursive=True): | |
resultCode, resultOutput = macSignBinary(file, args.cert) | |
if resultCode: | |
endMessage(False, f'there was a problem signing a file ({file}).\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
resultCode, resultOutput = macSignBinary(f'bin/{buildArch}/Deploy/{deploymentProject}.app', args.cert) | |
if resultCode: | |
endMessage(False, f'there was a problem signing a file ({file}).\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
# package the application into a zip file and notarize the application | |
startMessage('Creating zip archive...') | |
resultCode, resultOutput = execute(f'ditto -ck --sequesterRsrc --keepParent "bin/{buildArch}/Deploy/{deploymentProject}.app" "bin/{buildArch}/Deploy/{deploymentProject}.zip"') | |
if resultCode: | |
endMessage(False, f'there was a problem generating the application zip.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
startMessage('Performing notarization of application binary...') | |
status = notarizeFile(f'bin/{buildArch}/Deploy/{deploymentProject}.zip', args.appleid, args.password) | |
if not status=="success": | |
endMessage(False, f'there was a problem notarizing the application ({status}).') | |
exit(1) | |
endMessage(True) | |
startMessage('Stapling notarization ticket to binary...') | |
resultCode, resultOutput = execute(f'xcrun stapler staple "bin/{buildArch}/Deploy/{deploymentProject}.app"') | |
if resultCode: | |
endMessage(False, f'there was a problem stapling the ticket to application.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
# create dmg | |
startMessage('Creating installation dmg...') | |
resultCode, resultOutput = execute(f'tiffutil -cat "assets/[email protected]" "assets/[email protected]" -out "assets/dmg_background.tiff"') | |
if resultCode: | |
endMessage(False, f'there was a problem creating the combined tiff.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
resultCode, resultOutput = execute(f'tools/create-dmg/create-dmg --volname "{deploymentProject}" --background "./assets/dmg_background.tiff" --window-size 768 534 --icon-size 160 --icon "{deploymentProject}.app" 199 276 --app-drop-link 569 276 "./bin/{buildArch}/Deploy/{deploymentProject}.dmg" "./bin/{buildArch}/Deploy/{deploymentProject}.app"') | |
if resultCode: | |
endMessage(False, f'there was a problem creating the dmg.\r\n\r\n{resultOutput}\r\n') | |
print(f'tools/create-dmg/create-dmg --volname "{deploymentProject}" --background "./assets/dmg_background.tiff" --window-size 768 534 --icon-size 160 --icon "{deploymentProject}.app" 199 276 --app-drop-link 569 276 "./bin/{buildArch}/Deploy/{deploymentProject}.dmg" "./bin/{buildArch}/Deploy/{deploymentProject}.app"') | |
exit(1) | |
endMessage(True) | |
# sign the dmg and notarize it | |
startMessage('Signing dmg...') | |
resultCode, resultOutput = macSignBinary(f'./bin/{buildArch}/Deploy/{deploymentProject}.dmg', args.cert) | |
if resultCode: | |
endMessage(False, f'there was a problem signing the dmg.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
startMessage('Performing notarization of installation dmg...') | |
status = notarizeFile(f'bin/{buildArch}/Deploy/{deploymentProject}.dmg', args.appleid, args.password) | |
if not status=="success": | |
endMessage(False, f'there was a problem notarizing the dmg ({status}).') | |
exit(1) | |
endMessage(True) | |
startMessage('Stapling notarization ticket to dmg...') | |
resultCode, resultOutput = execute(f'xcrun stapler staple "bin/{buildArch}/Deploy/{deploymentProject}.dmg"') | |
if resultCode: | |
endMessage(False, f'there was a problem stapling the ticket to dmg.\r\n\r\n{resultOutput}\r\n') | |
exit(1) | |
endMessage(True) | |
startMessage('Copying dmg to deployment directory...') | |
shutil.copy2(f'bin/{buildArch}/Deploy/{deploymentProject}.dmg', f'deployment/{deploymentProject}.dmg') | |
endMessage(True) | |
endTime = time.time() | |
# done! | |
print(f'\r\n'+Style.BRIGHT+Fore.CYAN+f'Finished! Disk Image at "deployment/{deploymentProject}.dmg" is '+Fore.GREEN+'ready'+Fore.CYAN+' for distribution.') | |
print(Style.BRIGHT+f'\r\nTotal time taken to perform deployment was '+timeDelta(endTime-startTime)+'.') | |
exit(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment