mirror of
https://github.com/bitcoin/bitcoin.git
synced 2024-11-19 09:53:47 +01:00
59681beb89
1ac7b7f66b
scripts: filter more qt plugins we don't use in macdeployqtplus (fanquake)57cdd0697d
scripts: misc cleanups in macdeployqtplus (fanquake)51729a4dfa
scripts: use format() in macdeployqtplus (fanquake)1c37e81694
scripts: add type annotations to macdeployqtplus (fanquake) Pull request description: I frequently run `make deploy` while testing on macOS to get a properly light themed .app. With a brew installed Qt, this currently results in a pretty bloated executable: | branch | .app size | .dmg size | `make deploy` time | | ------- | --------- | --------- | --------------------- | | master (febf3a856b
) | 235mb | 86mb | 38s | | This PR (da98f6d470d236c027b7eb8b5f5552fdca04e803) | 51mb | 21mb | 22s | Similar change todd367ff8c9
. ```diff 'QtGui.framework'], 'pluginPath': '/usr/local/opt/qt/plugins', 'qtPath': '/usr/local/opt/qt'} -[('platforminputcontexts', 'libqtvirtualkeyboardplugin.dylib'), - ('geoservices', 'libqtgeoservices_esri.dylib'), - ('geoservices', 'libqtgeoservices_mapboxgl.dylib'), - ('geoservices', 'libqtgeoservices_nokia.dylib'), - ('geoservices', 'libqtgeoservices_itemsoverlay.dylib'), - ('geoservices', 'libqtgeoservices_osm.dylib'), - ('geoservices', 'libqtgeoservices_mapbox.dylib'), - ('sceneparsers', 'libgltfsceneexport.dylib'), - ('sceneparsers', 'libgltfsceneimport.dylib'), - ('platforms', 'libqwebgl.dylib'), +[('platforms', 'libqwebgl.dylib'), ('platforms', 'libqoffscreen.dylib'), ('platforms', 'libqminimal.dylib'), ('platforms', 'libqcocoa.dylib'), ('platformthemes', 'libqxdgdesktopportal.dylib'), - ('printsupport', 'libcocoaprintersupport.dylib'), - ('webview', 'libqtwebview_webengine.dylib'), - ('webview', 'libqtwebview_darwin.dylib'), - ('geometryloaders', 'libdefaultgeometryloader.dylib'), - ('geometryloaders', 'libgltfgeometryloader.dylib'), ('styles', 'libqmacstyle.dylib'), - ('canbus', 'libqttinycanbus.dylib'), - ('canbus', 'libqtpassthrucanbus.dylib'), - ('canbus', 'libqtvirtualcanbus.dylib'), - ('canbus', 'libqtpeakcanbus.dylib'), ('bearer', 'libqgenericbearer.dylib'), - ('imageformats', 'libqgif.dylib'), - ('imageformats', 'libqwbmp.dylib'), - ('imageformats', 'libqwebp.dylib'), - ('imageformats', 'libqico.dylib'), - ('imageformats', 'libqmacheif.dylib'), - ('imageformats', 'libqjpeg.dylib'), - ('imageformats', 'libqtiff.dylib'), - ('imageformats', 'libqicns.dylib'), - ('imageformats', 'libqtga.dylib'), - ('imageformats', 'libqmacjp2.dylib'), - ('texttospeech', 'libqtexttospeech_speechosx.dylib'), - ('generic', 'libqtuiotouchplugin.dylib'), - ('renderplugins', 'libscene2d.dylib'), - ('gamepads', 'libdarwingamepad.dylib'), - ('virtualkeyboard', 'libqtvirtualkeyboard_thai.dylib'), - ('virtualkeyboard', 'libqtvirtualkeyboard_openwnn.dylib'), - ('virtualkeyboard', 'libqtvirtualkeyboard_hangul.dylib'), - ('virtualkeyboard', 'libqtvirtualkeyboard_pinyin.dylib'), - ('virtualkeyboard', 'libqtvirtualkeyboard_tcime.dylib')] + ('generic', 'libqtuiotouchplugin.dylib')] ``` ACKs for top commit: laanwj: ACK1ac7b7f66b
(purely Python code review and the fact that this passes travis, cannot run this on a mac) dongcarl: tested ACK1ac7b7f66b
Tree-SHA512: 5974eeaf7229bb5bde2b283c1331ec57ee87f624db146401f6b77dee4ee5502e0bd669958a46205f10398a371f8e6c91ddacb9f0e1943f9f7d042fb6de7957a8
909 lines
37 KiB
Python
Executable File
909 lines
37 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (C) 2011 Patrick "p2k" Schneider <me@p2k-network.org>
|
|
#
|
|
# 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 subprocess, sys, re, os, shutil, stat, os.path, time
|
|
from string import Template
|
|
from argparse import ArgumentParser
|
|
from typing import List, Optional
|
|
|
|
# This is ported from the original macdeployqt with modifications
|
|
|
|
class FrameworkInfo(object):
|
|
def __init__(self):
|
|
self.frameworkDirectory = ""
|
|
self.frameworkName = ""
|
|
self.frameworkPath = ""
|
|
self.binaryDirectory = ""
|
|
self.binaryName = ""
|
|
self.binaryPath = ""
|
|
self.version = ""
|
|
self.installName = ""
|
|
self.deployedInstallName = ""
|
|
self.sourceFilePath = ""
|
|
self.destinationDirectory = ""
|
|
self.sourceResourcesDirectory = ""
|
|
self.sourceVersionContentsDirectory = ""
|
|
self.sourceContentsDirectory = ""
|
|
self.destinationResourcesDirectory = ""
|
|
self.destinationVersionContentsDirectory = ""
|
|
|
|
def __eq__(self, other):
|
|
if self.__class__ == other.__class__:
|
|
return self.__dict__ == other.__dict__
|
|
else:
|
|
return False
|
|
|
|
def __str__(self):
|
|
return """ Framework name: {}
|
|
Framework directory: {}
|
|
Framework path: {}
|
|
Binary name: {}
|
|
Binary directory: {}
|
|
Binary path: {}
|
|
Version: {}
|
|
Install name: {}
|
|
Deployed install name: {}
|
|
Source file Path: {}
|
|
Deployed Directory (relative to bundle): {}
|
|
""".format(self.frameworkName,
|
|
self.frameworkDirectory,
|
|
self.frameworkPath,
|
|
self.binaryName,
|
|
self.binaryDirectory,
|
|
self.binaryPath,
|
|
self.version,
|
|
self.installName,
|
|
self.deployedInstallName,
|
|
self.sourceFilePath,
|
|
self.destinationDirectory)
|
|
|
|
def isDylib(self):
|
|
return self.frameworkName.endswith(".dylib")
|
|
|
|
def isQtFramework(self):
|
|
if self.isDylib():
|
|
return self.frameworkName.startswith("libQt")
|
|
else:
|
|
return self.frameworkName.startswith("Qt")
|
|
|
|
reOLine = re.compile(r'^(.+) \(compatibility version [0-9.]+, current version [0-9.]+\)$')
|
|
bundleFrameworkDirectory = "Contents/Frameworks"
|
|
bundleBinaryDirectory = "Contents/MacOS"
|
|
|
|
@classmethod
|
|
def fromOtoolLibraryLine(cls, line: str) -> Optional['FrameworkInfo']:
|
|
# Note: line must be trimmed
|
|
if line == "":
|
|
return None
|
|
|
|
# Don't deploy system libraries (exception for libQtuitools and libQtlucene).
|
|
if line.startswith("/System/Library/") or line.startswith("@executable_path") or (line.startswith("/usr/lib/") and "libQt" not in line):
|
|
return None
|
|
|
|
m = cls.reOLine.match(line)
|
|
if m is None:
|
|
raise RuntimeError("otool line could not be parsed: " + line)
|
|
|
|
path = m.group(1)
|
|
|
|
info = cls()
|
|
info.sourceFilePath = path
|
|
info.installName = path
|
|
|
|
if path.endswith(".dylib"):
|
|
dirname, filename = os.path.split(path)
|
|
info.frameworkName = filename
|
|
info.frameworkDirectory = dirname
|
|
info.frameworkPath = path
|
|
|
|
info.binaryDirectory = dirname
|
|
info.binaryName = filename
|
|
info.binaryPath = path
|
|
info.version = "-"
|
|
|
|
info.installName = path
|
|
info.deployedInstallName = "@executable_path/../Frameworks/" + info.binaryName
|
|
info.sourceFilePath = path
|
|
info.destinationDirectory = cls.bundleFrameworkDirectory
|
|
else:
|
|
parts = path.split("/")
|
|
i = 0
|
|
# Search for the .framework directory
|
|
for part in parts:
|
|
if part.endswith(".framework"):
|
|
break
|
|
i += 1
|
|
if i == len(parts):
|
|
raise RuntimeError("Could not find .framework or .dylib in otool line: " + line)
|
|
|
|
info.frameworkName = parts[i]
|
|
info.frameworkDirectory = "/".join(parts[:i])
|
|
info.frameworkPath = os.path.join(info.frameworkDirectory, info.frameworkName)
|
|
|
|
info.binaryName = parts[i+3]
|
|
info.binaryDirectory = "/".join(parts[i+1:i+3])
|
|
info.binaryPath = os.path.join(info.binaryDirectory, info.binaryName)
|
|
info.version = parts[i+2]
|
|
|
|
info.deployedInstallName = "@executable_path/../Frameworks/" + os.path.join(info.frameworkName, info.binaryPath)
|
|
info.destinationDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, info.binaryDirectory)
|
|
|
|
info.sourceResourcesDirectory = os.path.join(info.frameworkPath, "Resources")
|
|
info.sourceContentsDirectory = os.path.join(info.frameworkPath, "Contents")
|
|
info.sourceVersionContentsDirectory = os.path.join(info.frameworkPath, "Versions", info.version, "Contents")
|
|
info.destinationResourcesDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Resources")
|
|
info.destinationVersionContentsDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Versions", info.version, "Contents")
|
|
|
|
return info
|
|
|
|
class ApplicationBundleInfo(object):
|
|
def __init__(self, path: str):
|
|
self.path = path
|
|
appName = "Bitcoin-Qt"
|
|
self.binaryPath = os.path.join(path, "Contents", "MacOS", appName)
|
|
if not os.path.exists(self.binaryPath):
|
|
raise RuntimeError("Could not find bundle binary for " + path)
|
|
self.resourcesPath = os.path.join(path, "Contents", "Resources")
|
|
self.pluginPath = os.path.join(path, "Contents", "PlugIns")
|
|
|
|
class DeploymentInfo(object):
|
|
def __init__(self):
|
|
self.qtPath = None
|
|
self.pluginPath = None
|
|
self.deployedFrameworks = []
|
|
|
|
def detectQtPath(self, frameworkDirectory: str):
|
|
parentDir = os.path.dirname(frameworkDirectory)
|
|
if os.path.exists(os.path.join(parentDir, "translations")):
|
|
# Classic layout, e.g. "/usr/local/Trolltech/Qt-4.x.x"
|
|
self.qtPath = parentDir
|
|
else:
|
|
self.qtPath = os.getenv("QTDIR", None)
|
|
|
|
if self.qtPath is not None:
|
|
pluginPath = os.path.join(self.qtPath, "plugins")
|
|
if os.path.exists(pluginPath):
|
|
self.pluginPath = pluginPath
|
|
|
|
def usesFramework(self, name: str) -> bool:
|
|
nameDot = "{}.".format(name)
|
|
libNameDot = "lib{}.".format(name)
|
|
for framework in self.deployedFrameworks:
|
|
if framework.endswith(".framework"):
|
|
if framework.startswith(nameDot):
|
|
return True
|
|
elif framework.endswith(".dylib"):
|
|
if framework.startswith(libNameDot):
|
|
return True
|
|
return False
|
|
|
|
def getFrameworks(binaryPath: str, verbose: int) -> List[FrameworkInfo]:
|
|
if verbose >= 3:
|
|
print("Inspecting with otool: " + binaryPath)
|
|
otoolbin=os.getenv("OTOOL", "otool")
|
|
otool = subprocess.Popen([otoolbin, "-L", binaryPath], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
|
o_stdout, o_stderr = otool.communicate()
|
|
if otool.returncode != 0:
|
|
if verbose >= 1:
|
|
sys.stderr.write(o_stderr)
|
|
sys.stderr.flush()
|
|
raise RuntimeError("otool failed with return code {}".format(otool.returncode))
|
|
|
|
otoolLines = o_stdout.split("\n")
|
|
otoolLines.pop(0) # First line is the inspected binary
|
|
if ".framework" in binaryPath or binaryPath.endswith(".dylib"):
|
|
otoolLines.pop(0) # Frameworks and dylibs list themselves as a dependency.
|
|
|
|
libraries = []
|
|
for line in otoolLines:
|
|
line = line.replace("@loader_path", os.path.dirname(binaryPath))
|
|
info = FrameworkInfo.fromOtoolLibraryLine(line.strip())
|
|
if info is not None:
|
|
if verbose >= 3:
|
|
print("Found framework:")
|
|
print(info)
|
|
libraries.append(info)
|
|
|
|
return libraries
|
|
|
|
def runInstallNameTool(action: str, *args):
|
|
installnametoolbin=os.getenv("INSTALLNAMETOOL", "install_name_tool")
|
|
subprocess.check_call([installnametoolbin, "-"+action] + list(args))
|
|
|
|
def changeInstallName(oldName: str, newName: str, binaryPath: str, verbose: int):
|
|
if verbose >= 3:
|
|
print("Using install_name_tool:")
|
|
print(" in", binaryPath)
|
|
print(" change reference", oldName)
|
|
print(" to", newName)
|
|
runInstallNameTool("change", oldName, newName, binaryPath)
|
|
|
|
def changeIdentification(id: str, binaryPath: str, verbose: int):
|
|
if verbose >= 3:
|
|
print("Using install_name_tool:")
|
|
print(" change identification in", binaryPath)
|
|
print(" to", id)
|
|
runInstallNameTool("id", id, binaryPath)
|
|
|
|
def runStrip(binaryPath: str, verbose: int):
|
|
stripbin=os.getenv("STRIP", "strip")
|
|
if verbose >= 3:
|
|
print("Using strip:")
|
|
print(" stripped", binaryPath)
|
|
subprocess.check_call([stripbin, "-x", binaryPath])
|
|
|
|
def copyFramework(framework: FrameworkInfo, path: str, verbose: int) -> Optional[str]:
|
|
if framework.sourceFilePath.startswith("Qt"):
|
|
#standard place for Nokia Qt installer's frameworks
|
|
fromPath = "/Library/Frameworks/" + framework.sourceFilePath
|
|
else:
|
|
fromPath = framework.sourceFilePath
|
|
toDir = os.path.join(path, framework.destinationDirectory)
|
|
toPath = os.path.join(toDir, framework.binaryName)
|
|
|
|
if not os.path.exists(fromPath):
|
|
raise RuntimeError("No file at " + fromPath)
|
|
|
|
if os.path.exists(toPath):
|
|
return None # Already there
|
|
|
|
if not os.path.exists(toDir):
|
|
os.makedirs(toDir)
|
|
|
|
shutil.copy2(fromPath, toPath)
|
|
if verbose >= 3:
|
|
print("Copied:", fromPath)
|
|
print(" to:", toPath)
|
|
|
|
permissions = os.stat(toPath)
|
|
if not permissions.st_mode & stat.S_IWRITE:
|
|
os.chmod(toPath, permissions.st_mode | stat.S_IWRITE)
|
|
|
|
if not framework.isDylib(): # Copy resources for real frameworks
|
|
|
|
linkfrom = os.path.join(path, "Contents","Frameworks", framework.frameworkName, "Versions", "Current")
|
|
linkto = framework.version
|
|
if not os.path.exists(linkfrom):
|
|
os.symlink(linkto, linkfrom)
|
|
if verbose >= 2:
|
|
print("Linked:", linkfrom, "->", linkto)
|
|
fromResourcesDir = framework.sourceResourcesDirectory
|
|
if os.path.exists(fromResourcesDir):
|
|
toResourcesDir = os.path.join(path, framework.destinationResourcesDirectory)
|
|
shutil.copytree(fromResourcesDir, toResourcesDir, symlinks=True)
|
|
if verbose >= 3:
|
|
print("Copied resources:", fromResourcesDir)
|
|
print(" to:", toResourcesDir)
|
|
fromContentsDir = framework.sourceVersionContentsDirectory
|
|
if not os.path.exists(fromContentsDir):
|
|
fromContentsDir = framework.sourceContentsDirectory
|
|
if os.path.exists(fromContentsDir):
|
|
toContentsDir = os.path.join(path, framework.destinationVersionContentsDirectory)
|
|
shutil.copytree(fromContentsDir, toContentsDir, symlinks=True)
|
|
if verbose >= 3:
|
|
print("Copied Contents:", fromContentsDir)
|
|
print(" to:", toContentsDir)
|
|
elif framework.frameworkName.startswith("libQtGui"): # Copy qt_menu.nib (applies to non-framework layout)
|
|
qtMenuNibSourcePath = os.path.join(framework.frameworkDirectory, "Resources", "qt_menu.nib")
|
|
qtMenuNibDestinationPath = os.path.join(path, "Contents", "Resources", "qt_menu.nib")
|
|
if os.path.exists(qtMenuNibSourcePath) and not os.path.exists(qtMenuNibDestinationPath):
|
|
shutil.copytree(qtMenuNibSourcePath, qtMenuNibDestinationPath, symlinks=True)
|
|
if verbose >= 3:
|
|
print("Copied for libQtGui:", qtMenuNibSourcePath)
|
|
print(" to:", qtMenuNibDestinationPath)
|
|
|
|
return toPath
|
|
|
|
def deployFrameworks(frameworks: List[FrameworkInfo], bundlePath: str, binaryPath: str, strip: bool, verbose: int, deploymentInfo: Optional[DeploymentInfo] = None) -> DeploymentInfo:
|
|
if deploymentInfo is None:
|
|
deploymentInfo = DeploymentInfo()
|
|
|
|
while len(frameworks) > 0:
|
|
framework = frameworks.pop(0)
|
|
deploymentInfo.deployedFrameworks.append(framework.frameworkName)
|
|
|
|
if verbose >= 2:
|
|
print("Processing", framework.frameworkName, "...")
|
|
|
|
# Get the Qt path from one of the Qt frameworks
|
|
if deploymentInfo.qtPath is None and framework.isQtFramework():
|
|
deploymentInfo.detectQtPath(framework.frameworkDirectory)
|
|
|
|
if framework.installName.startswith("@executable_path") or framework.installName.startswith(bundlePath):
|
|
if verbose >= 2:
|
|
print(framework.frameworkName, "already deployed, skipping.")
|
|
continue
|
|
|
|
# install_name_tool the new id into the binary
|
|
changeInstallName(framework.installName, framework.deployedInstallName, binaryPath, verbose)
|
|
|
|
# Copy framework to app bundle.
|
|
deployedBinaryPath = copyFramework(framework, bundlePath, verbose)
|
|
# Skip the rest if already was deployed.
|
|
if deployedBinaryPath is None:
|
|
continue
|
|
|
|
if strip:
|
|
runStrip(deployedBinaryPath, verbose)
|
|
|
|
# install_name_tool it a new id.
|
|
changeIdentification(framework.deployedInstallName, deployedBinaryPath, verbose)
|
|
# Check for framework dependencies
|
|
dependencies = getFrameworks(deployedBinaryPath, verbose)
|
|
|
|
for dependency in dependencies:
|
|
changeInstallName(dependency.installName, dependency.deployedInstallName, deployedBinaryPath, verbose)
|
|
|
|
# Deploy framework if necessary.
|
|
if dependency.frameworkName not in deploymentInfo.deployedFrameworks and dependency not in frameworks:
|
|
frameworks.append(dependency)
|
|
|
|
return deploymentInfo
|
|
|
|
def deployFrameworksForAppBundle(applicationBundle: ApplicationBundleInfo, strip: bool, verbose: int) -> DeploymentInfo:
|
|
frameworks = getFrameworks(applicationBundle.binaryPath, verbose)
|
|
if len(frameworks) == 0 and verbose >= 1:
|
|
print("Warning: Could not find any external frameworks to deploy in {}.".format(applicationBundle.path))
|
|
return DeploymentInfo()
|
|
else:
|
|
return deployFrameworks(frameworks, applicationBundle.path, applicationBundle.binaryPath, strip, verbose)
|
|
|
|
def deployPlugins(appBundleInfo: ApplicationBundleInfo, deploymentInfo: DeploymentInfo, strip: bool, verbose: int):
|
|
# Lookup available plugins, exclude unneeded
|
|
plugins = []
|
|
if deploymentInfo.pluginPath is None:
|
|
return
|
|
for dirpath, dirnames, filenames in os.walk(deploymentInfo.pluginPath):
|
|
pluginDirectory = os.path.relpath(dirpath, deploymentInfo.pluginPath)
|
|
if pluginDirectory == "designer":
|
|
# Skip designer plugins
|
|
continue
|
|
elif pluginDirectory == "printsupport":
|
|
# Skip printsupport plugins
|
|
continue
|
|
elif pluginDirectory == "imageformats":
|
|
# Skip imageformats plugins
|
|
continue
|
|
elif pluginDirectory == "sqldrivers":
|
|
# Deploy the sql plugins only if QtSql is in use
|
|
if not deploymentInfo.usesFramework("QtSql"):
|
|
continue
|
|
elif pluginDirectory == "script":
|
|
# Deploy the script plugins only if QtScript is in use
|
|
if not deploymentInfo.usesFramework("QtScript"):
|
|
continue
|
|
elif pluginDirectory == "qmltooling" or pluginDirectory == "qml1tooling":
|
|
# Deploy the qml plugins only if QtDeclarative is in use
|
|
if not deploymentInfo.usesFramework("QtDeclarative"):
|
|
continue
|
|
elif pluginDirectory == "bearer":
|
|
# Deploy the bearer plugins only if QtNetwork is in use
|
|
if not deploymentInfo.usesFramework("QtNetwork"):
|
|
continue
|
|
elif pluginDirectory == "position":
|
|
# Deploy the position plugins only if QtPositioning is in use
|
|
if not deploymentInfo.usesFramework("QtPositioning"):
|
|
continue
|
|
elif pluginDirectory == "sensors" or pluginDirectory == "sensorgestures":
|
|
# Deploy the sensor plugins only if QtSensors is in use
|
|
if not deploymentInfo.usesFramework("QtSensors"):
|
|
continue
|
|
elif pluginDirectory == "audio" or pluginDirectory == "playlistformats":
|
|
# Deploy the audio plugins only if QtMultimedia is in use
|
|
if not deploymentInfo.usesFramework("QtMultimedia"):
|
|
continue
|
|
elif pluginDirectory == "mediaservice":
|
|
# Deploy the mediaservice plugins only if QtMultimediaWidgets is in use
|
|
if not deploymentInfo.usesFramework("QtMultimediaWidgets"):
|
|
continue
|
|
elif pluginDirectory == "canbus":
|
|
# Deploy the canbus plugins only if QtSerialBus is in use
|
|
if not deploymentInfo.usesFramework("QtSerialBus"):
|
|
continue
|
|
elif pluginDirectory == "webview":
|
|
# Deploy the webview plugins only if QtWebView is in use
|
|
if not deploymentInfo.usesFramework("QtWebView"):
|
|
continue
|
|
elif pluginDirectory == "gamepads":
|
|
# Deploy the webview plugins only if QtGamepad is in use
|
|
if not deploymentInfo.usesFramework("QtGamepad"):
|
|
continue
|
|
elif pluginDirectory == "geoservices":
|
|
# Deploy the webview plugins only if QtLocation is in use
|
|
if not deploymentInfo.usesFramework("QtLocation"):
|
|
continue
|
|
elif pluginDirectory == "texttospeech":
|
|
# Deploy the texttospeech plugins only if QtTextToSpeech is in use
|
|
if not deploymentInfo.usesFramework("QtTextToSpeech"):
|
|
continue
|
|
elif pluginDirectory == "virtualkeyboard":
|
|
# Deploy the virtualkeyboard plugins only if QtVirtualKeyboard is in use
|
|
if not deploymentInfo.usesFramework("QtVirtualKeyboard"):
|
|
continue
|
|
elif pluginDirectory == "sceneparsers":
|
|
# Deploy the virtualkeyboard plugins only if Qt3DCore is in use
|
|
if not deploymentInfo.usesFramework("Qt3DCore"):
|
|
continue
|
|
elif pluginDirectory == "renderplugins":
|
|
# Deploy the renderplugins plugins only if Qt3DCore is in use
|
|
if not deploymentInfo.usesFramework("Qt3DCore"):
|
|
continue
|
|
elif pluginDirectory == "geometryloaders":
|
|
# Deploy the geometryloaders plugins only if Qt3DCore is in use
|
|
if not deploymentInfo.usesFramework("Qt3DCore"):
|
|
continue
|
|
|
|
for pluginName in filenames:
|
|
pluginPath = os.path.join(pluginDirectory, pluginName)
|
|
if pluginName.endswith("_debug.dylib"):
|
|
# Skip debug plugins
|
|
continue
|
|
elif pluginPath == "imageformats/libqsvg.dylib" or pluginPath == "iconengines/libqsvgicon.dylib":
|
|
# Deploy the svg plugins only if QtSvg is in use
|
|
if not deploymentInfo.usesFramework("QtSvg"):
|
|
continue
|
|
elif pluginPath == "accessible/libqtaccessiblecompatwidgets.dylib":
|
|
# Deploy accessibility for Qt3Support only if the Qt3Support is in use
|
|
if not deploymentInfo.usesFramework("Qt3Support"):
|
|
continue
|
|
elif pluginPath == "graphicssystems/libqglgraphicssystem.dylib":
|
|
# Deploy the opengl graphicssystem plugin only if QtOpenGL is in use
|
|
if not deploymentInfo.usesFramework("QtOpenGL"):
|
|
continue
|
|
elif pluginPath == "accessible/libqtaccessiblequick.dylib":
|
|
# Deploy the accessible qtquick plugin only if QtQuick is in use
|
|
if not deploymentInfo.usesFramework("QtQuick"):
|
|
continue
|
|
elif pluginPath == "platforminputcontexts/libqtvirtualkeyboardplugin.dylib":
|
|
# Deploy the virtualkeyboardplugin plugin only if QtVirtualKeyboard is in use
|
|
if not deploymentInfo.usesFramework("QtVirtualKeyboard"):
|
|
continue
|
|
|
|
plugins.append((pluginDirectory, pluginName))
|
|
|
|
for pluginDirectory, pluginName in plugins:
|
|
if verbose >= 2:
|
|
print("Processing plugin", os.path.join(pluginDirectory, pluginName), "...")
|
|
|
|
sourcePath = os.path.join(deploymentInfo.pluginPath, pluginDirectory, pluginName)
|
|
destinationDirectory = os.path.join(appBundleInfo.pluginPath, pluginDirectory)
|
|
if not os.path.exists(destinationDirectory):
|
|
os.makedirs(destinationDirectory)
|
|
|
|
destinationPath = os.path.join(destinationDirectory, pluginName)
|
|
shutil.copy2(sourcePath, destinationPath)
|
|
if verbose >= 3:
|
|
print("Copied:", sourcePath)
|
|
print(" to:", destinationPath)
|
|
|
|
if strip:
|
|
runStrip(destinationPath, verbose)
|
|
|
|
dependencies = getFrameworks(destinationPath, verbose)
|
|
|
|
for dependency in dependencies:
|
|
changeInstallName(dependency.installName, dependency.deployedInstallName, destinationPath, verbose)
|
|
|
|
# Deploy framework if necessary.
|
|
if dependency.frameworkName not in deploymentInfo.deployedFrameworks:
|
|
deployFrameworks([dependency], appBundleInfo.path, destinationPath, strip, verbose, deploymentInfo)
|
|
|
|
qt_conf="""[Paths]
|
|
Translations=Resources
|
|
Plugins=PlugIns
|
|
"""
|
|
|
|
ap = ArgumentParser(description="""Improved version of macdeployqt.
|
|
|
|
Outputs a ready-to-deploy app in a folder "dist" and optionally wraps it in a .dmg file.
|
|
Note, that the "dist" folder will be deleted before deploying on each run.
|
|
|
|
Optionally, Qt translation files (.qm) and additional resources can be added to the bundle.
|
|
|
|
Also optionally signs the .app bundle; set the CODESIGNARGS environment variable to pass arguments
|
|
to the codesign tool.
|
|
E.g. CODESIGNARGS='--sign "Developer ID Application: ..." --keychain /encrypted/foo.keychain'""")
|
|
|
|
ap.add_argument("app_bundle", nargs=1, metavar="app-bundle", help="application bundle to be deployed")
|
|
ap.add_argument("-verbose", type=int, nargs=1, default=[1], metavar="<0-3>", help="0 = no output, 1 = error/warning (default), 2 = normal, 3 = debug")
|
|
ap.add_argument("-no-plugins", dest="plugins", action="store_false", default=True, help="skip plugin deployment")
|
|
ap.add_argument("-no-strip", dest="strip", action="store_false", default=True, help="don't run 'strip' on the binaries")
|
|
ap.add_argument("-sign", dest="sign", action="store_true", default=False, help="sign .app bundle with codesign tool")
|
|
ap.add_argument("-dmg", nargs="?", const="", metavar="basename", help="create a .dmg disk image; if basename is not specified, a camel-cased version of the app name is used")
|
|
ap.add_argument("-fancy", nargs=1, metavar="plist", default=[], help="make a fancy looking disk image using the given plist file with instructions; requires -dmg to work")
|
|
ap.add_argument("-add-qt-tr", nargs=1, metavar="languages", default=[], help="add Qt translation files to the bundle's resources; the language list must be separated with commas, not with whitespace")
|
|
ap.add_argument("-translations-dir", nargs=1, metavar="path", default=None, help="Path to Qt's translation files")
|
|
ap.add_argument("-add-resources", nargs="+", metavar="path", default=[], help="list of additional files or folders to be copied into the bundle's resources; must be the last argument")
|
|
ap.add_argument("-volname", nargs=1, metavar="volname", default=[], help="custom volume name for dmg")
|
|
|
|
config = ap.parse_args()
|
|
|
|
verbose = config.verbose[0]
|
|
|
|
# ------------------------------------------------
|
|
|
|
app_bundle = config.app_bundle[0]
|
|
|
|
if not os.path.exists(app_bundle):
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not find app bundle \"{}\"\n".format(app_bundle))
|
|
sys.exit(1)
|
|
|
|
app_bundle_name = os.path.splitext(os.path.basename(app_bundle))[0]
|
|
|
|
# ------------------------------------------------
|
|
translations_dir = None
|
|
if config.translations_dir and config.translations_dir[0]:
|
|
if os.path.exists(config.translations_dir[0]):
|
|
translations_dir = config.translations_dir[0]
|
|
else:
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not find translation dir \"{}\"\n".format(translations_dir))
|
|
sys.exit(1)
|
|
# ------------------------------------------------
|
|
|
|
for p in config.add_resources:
|
|
if verbose >= 3:
|
|
print("Checking for \"%s\"..." % p)
|
|
if not os.path.exists(p):
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not find additional resource file \"{}\"\n".format(p))
|
|
sys.exit(1)
|
|
|
|
# ------------------------------------------------
|
|
|
|
if len(config.fancy) == 1:
|
|
if verbose >= 3:
|
|
print("Fancy: Importing plistlib...")
|
|
try:
|
|
import plistlib
|
|
except ImportError:
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not import plistlib which is required for fancy disk images.\n")
|
|
sys.exit(1)
|
|
|
|
p = config.fancy[0]
|
|
if verbose >= 3:
|
|
print("Fancy: Loading \"{}\"...".format(p))
|
|
if not os.path.exists(p):
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not find fancy disk image plist at \"{}\"\n".format(p))
|
|
sys.exit(1)
|
|
|
|
try:
|
|
fancy = plistlib.readPlist(p)
|
|
except:
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not parse fancy disk image plist at \"{}\"\n".format(p))
|
|
sys.exit(1)
|
|
|
|
try:
|
|
assert "window_bounds" not in fancy or (isinstance(fancy["window_bounds"], list) and len(fancy["window_bounds"]) == 4)
|
|
assert "background_picture" not in fancy or isinstance(fancy["background_picture"], str)
|
|
assert "icon_size" not in fancy or isinstance(fancy["icon_size"], int)
|
|
assert "applications_symlink" not in fancy or isinstance(fancy["applications_symlink"], bool)
|
|
if "items_position" in fancy:
|
|
assert isinstance(fancy["items_position"], dict)
|
|
for key, value in fancy["items_position"].items():
|
|
assert isinstance(value, list) and len(value) == 2 and isinstance(value[0], int) and isinstance(value[1], int)
|
|
except:
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Bad format of fancy disk image plist at \"{}\"\n".format(p))
|
|
sys.exit(1)
|
|
|
|
if "background_picture" in fancy:
|
|
bp = fancy["background_picture"]
|
|
if verbose >= 3:
|
|
print("Fancy: Resolving background picture \"{}\"...".format(bp))
|
|
if not os.path.exists(bp):
|
|
bp = os.path.join(os.path.dirname(p), bp)
|
|
if not os.path.exists(bp):
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not find background picture at \"{}\" or \"{}\"\n".format(fancy["background_picture"], bp))
|
|
sys.exit(1)
|
|
else:
|
|
fancy["background_picture"] = bp
|
|
else:
|
|
fancy = None
|
|
|
|
# ------------------------------------------------
|
|
|
|
if os.path.exists("dist"):
|
|
if verbose >= 2:
|
|
print("+ Removing old dist folder +")
|
|
|
|
shutil.rmtree("dist")
|
|
|
|
# ------------------------------------------------
|
|
|
|
if len(config.volname) == 1:
|
|
volname = config.volname[0]
|
|
else:
|
|
volname = app_bundle_name
|
|
|
|
# ------------------------------------------------
|
|
|
|
target = os.path.join("dist", "Bitcoin-Qt.app")
|
|
|
|
if verbose >= 2:
|
|
print("+ Copying source bundle +")
|
|
if verbose >= 3:
|
|
print(app_bundle, "->", target)
|
|
|
|
os.mkdir("dist")
|
|
shutil.copytree(app_bundle, target, symlinks=True)
|
|
|
|
applicationBundle = ApplicationBundleInfo(target)
|
|
|
|
# ------------------------------------------------
|
|
|
|
if verbose >= 2:
|
|
print("+ Deploying frameworks +")
|
|
|
|
try:
|
|
deploymentInfo = deployFrameworksForAppBundle(applicationBundle, config.strip, verbose)
|
|
if deploymentInfo.qtPath is None:
|
|
deploymentInfo.qtPath = os.getenv("QTDIR", None)
|
|
if deploymentInfo.qtPath is None:
|
|
if verbose >= 1:
|
|
sys.stderr.write("Warning: Could not detect Qt's path, skipping plugin deployment!\n")
|
|
config.plugins = False
|
|
except RuntimeError as e:
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: {}\n".format(str(e)))
|
|
sys.exit(1)
|
|
|
|
# ------------------------------------------------
|
|
|
|
if config.plugins:
|
|
if verbose >= 2:
|
|
print("+ Deploying plugins +")
|
|
|
|
try:
|
|
deployPlugins(applicationBundle, deploymentInfo, config.strip, verbose)
|
|
except RuntimeError as e:
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: {}\n".format(str(e)))
|
|
sys.exit(1)
|
|
|
|
# ------------------------------------------------
|
|
|
|
if len(config.add_qt_tr) == 0:
|
|
add_qt_tr = []
|
|
else:
|
|
if translations_dir is not None:
|
|
qt_tr_dir = translations_dir
|
|
else:
|
|
if deploymentInfo.qtPath is not None:
|
|
qt_tr_dir = os.path.join(deploymentInfo.qtPath, "translations")
|
|
else:
|
|
sys.stderr.write("Error: Could not find Qt translation path\n")
|
|
sys.exit(1)
|
|
add_qt_tr = ["qt_{}.qm".format(lng) for lng in config.add_qt_tr[0].split(",")]
|
|
for lng_file in add_qt_tr:
|
|
p = os.path.join(qt_tr_dir, lng_file)
|
|
if verbose >= 3:
|
|
print("Checking for \"{}\"...".format(p))
|
|
if not os.path.exists(p):
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not find Qt translation file \"{}\"\n".format(lng_file))
|
|
sys.exit(1)
|
|
|
|
# ------------------------------------------------
|
|
|
|
if verbose >= 2:
|
|
print("+ Installing qt.conf +")
|
|
|
|
with open(os.path.join(applicationBundle.resourcesPath, "qt.conf"), "wb") as f:
|
|
f.write(qt_conf.encode())
|
|
|
|
# ------------------------------------------------
|
|
|
|
if len(add_qt_tr) > 0 and verbose >= 2:
|
|
print("+ Adding Qt translations +")
|
|
|
|
for lng_file in add_qt_tr:
|
|
if verbose >= 3:
|
|
print(os.path.join(qt_tr_dir, lng_file), "->", os.path.join(applicationBundle.resourcesPath, lng_file))
|
|
shutil.copy2(os.path.join(qt_tr_dir, lng_file), os.path.join(applicationBundle.resourcesPath, lng_file))
|
|
|
|
# ------------------------------------------------
|
|
|
|
if len(config.add_resources) > 0 and verbose >= 2:
|
|
print("+ Adding additional resources +")
|
|
|
|
for p in config.add_resources:
|
|
t = os.path.join(applicationBundle.resourcesPath, os.path.basename(p))
|
|
if verbose >= 3:
|
|
print(p, "->", t)
|
|
if os.path.isdir(p):
|
|
shutil.copytree(p, t, symlinks=True)
|
|
else:
|
|
shutil.copy2(p, t)
|
|
|
|
# ------------------------------------------------
|
|
|
|
if config.sign and 'CODESIGNARGS' not in os.environ:
|
|
print("You must set the CODESIGNARGS environment variable. Skipping signing.")
|
|
elif config.sign:
|
|
if verbose >= 1:
|
|
print("Code-signing app bundle {}".format(target))
|
|
subprocess.check_call("codesign --force {} {}".format(os.environ['CODESIGNARGS'], target), shell=True)
|
|
|
|
# ------------------------------------------------
|
|
|
|
if config.dmg is not None:
|
|
|
|
def runHDIUtil(verb: str, image_basename: str, **kwargs) -> int:
|
|
hdiutil_args = ["hdiutil", verb, image_basename + ".dmg"]
|
|
if "capture_stdout" in kwargs:
|
|
del kwargs["capture_stdout"]
|
|
run = subprocess.check_output
|
|
else:
|
|
if verbose < 2:
|
|
hdiutil_args.append("-quiet")
|
|
elif verbose >= 3:
|
|
hdiutil_args.append("-verbose")
|
|
run = subprocess.check_call
|
|
|
|
for key, value in kwargs.items():
|
|
hdiutil_args.append("-" + key)
|
|
if value is not True:
|
|
hdiutil_args.append(str(value))
|
|
|
|
return run(hdiutil_args, universal_newlines=True)
|
|
|
|
if verbose >= 2:
|
|
if fancy is None:
|
|
print("+ Creating .dmg disk image +")
|
|
else:
|
|
print("+ Preparing .dmg disk image +")
|
|
|
|
if config.dmg != "":
|
|
dmg_name = config.dmg
|
|
else:
|
|
spl = app_bundle_name.split(" ")
|
|
dmg_name = spl[0] + "".join(p.capitalize() for p in spl[1:])
|
|
|
|
if fancy is None:
|
|
try:
|
|
runHDIUtil("create", dmg_name, srcfolder="dist", format="UDBZ", volname=volname, ov=True)
|
|
except subprocess.CalledProcessError as e:
|
|
sys.exit(e.returncode)
|
|
else:
|
|
if verbose >= 3:
|
|
print("Determining size of \"dist\"...")
|
|
size = 0
|
|
for path, dirs, files in os.walk("dist"):
|
|
for file in files:
|
|
size += os.path.getsize(os.path.join(path, file))
|
|
size += int(size * 0.15)
|
|
|
|
if verbose >= 3:
|
|
print("Creating temp image for modification...")
|
|
try:
|
|
runHDIUtil("create", dmg_name + ".temp", srcfolder="dist", format="UDRW", size=size, volname=volname, ov=True)
|
|
except subprocess.CalledProcessError as e:
|
|
sys.exit(e.returncode)
|
|
|
|
if verbose >= 3:
|
|
print("Attaching temp image...")
|
|
try:
|
|
output = runHDIUtil("attach", dmg_name + ".temp", readwrite=True, noverify=True, noautoopen=True, capture_stdout=True)
|
|
except subprocess.CalledProcessError as e:
|
|
sys.exit(e.returncode)
|
|
|
|
m = re.search(r"/Volumes/(.+$)", output)
|
|
disk_root = m.group(0)
|
|
disk_name = m.group(1)
|
|
|
|
if verbose >= 2:
|
|
print("+ Applying fancy settings +")
|
|
|
|
if "background_picture" in fancy:
|
|
bg_path = os.path.join(disk_root, ".background", os.path.basename(fancy["background_picture"]))
|
|
os.mkdir(os.path.dirname(bg_path))
|
|
if verbose >= 3:
|
|
print(fancy["background_picture"], "->", bg_path)
|
|
shutil.copy2(fancy["background_picture"], bg_path)
|
|
else:
|
|
bg_path = None
|
|
|
|
if fancy.get("applications_symlink", False):
|
|
os.symlink("/Applications", os.path.join(disk_root, "Applications"))
|
|
|
|
# The Python appscript package broke with OSX 10.8 and isn't being fixed.
|
|
# So we now build up an AppleScript string and use the osascript command
|
|
# to make the .dmg file pretty:
|
|
appscript = Template( """
|
|
on run argv
|
|
tell application "Finder"
|
|
tell disk "$disk"
|
|
open
|
|
set current view of container window to icon view
|
|
set toolbar visible of container window to false
|
|
set statusbar visible of container window to false
|
|
set the bounds of container window to {$window_bounds}
|
|
set theViewOptions to the icon view options of container window
|
|
set arrangement of theViewOptions to not arranged
|
|
set icon size of theViewOptions to $icon_size
|
|
$background_commands
|
|
$items_positions
|
|
close -- close/reopen works around a bug...
|
|
open
|
|
update without registering applications
|
|
delay 5
|
|
eject
|
|
end tell
|
|
end tell
|
|
end run
|
|
""")
|
|
|
|
itemscript = Template('set position of item "${item}" of container window to {${position}}')
|
|
items_positions = []
|
|
if "items_position" in fancy:
|
|
for name, position in fancy["items_position"].items():
|
|
params = { "item" : name, "position" : ",".join([str(p) for p in position]) }
|
|
items_positions.append(itemscript.substitute(params))
|
|
|
|
params = {
|
|
"disk" : volname,
|
|
"window_bounds" : "300,300,800,620",
|
|
"icon_size" : "96",
|
|
"background_commands" : "",
|
|
"items_positions" : "\n ".join(items_positions)
|
|
}
|
|
if "window_bounds" in fancy:
|
|
params["window_bounds"] = ",".join([str(p) for p in fancy["window_bounds"]])
|
|
if "icon_size" in fancy:
|
|
params["icon_size"] = str(fancy["icon_size"])
|
|
if bg_path is not None:
|
|
# Set background file, then call SetFile to make it invisible.
|
|
# (note: making it invisible first makes set background picture fail)
|
|
bgscript = Template("""set background picture of theViewOptions to file ".background:$bgpic"
|
|
do shell script "SetFile -a V /Volumes/$disk/.background/$bgpic" """)
|
|
params["background_commands"] = bgscript.substitute({"bgpic" : os.path.basename(bg_path), "disk" : params["disk"]})
|
|
|
|
s = appscript.substitute(params)
|
|
if verbose >= 2:
|
|
print("Running AppleScript:")
|
|
print(s)
|
|
|
|
p = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE)
|
|
p.communicate(input=s.encode('utf-8'))
|
|
if p.returncode:
|
|
print("Error running osascript.")
|
|
|
|
if verbose >= 2:
|
|
print("+ Finalizing .dmg disk image +")
|
|
time.sleep(5)
|
|
|
|
try:
|
|
runHDIUtil("convert", dmg_name + ".temp", format="UDBZ", o=dmg_name + ".dmg", ov=True)
|
|
except subprocess.CalledProcessError as e:
|
|
sys.exit(e.returncode)
|
|
|
|
os.unlink(dmg_name + ".temp.dmg")
|
|
|
|
# ------------------------------------------------
|
|
|
|
if verbose >= 2:
|
|
print("+ Done +")
|
|
|
|
sys.exit(0)
|