bitcoin/contrib/macdeploy/macdeployqtplus
fanquake 59681beb89
Merge #16477: build: skip deploying plugins we dont use in macdeployqtplus
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 to dd367ff8c9.

  ```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:
    ACK 1ac7b7f66b (purely Python code review and the fact that this passes travis, cannot run this on a mac)
  dongcarl:
    tested ACK 1ac7b7f66b

Tree-SHA512: 5974eeaf7229bb5bde2b283c1331ec57ee87f624db146401f6b77dee4ee5502e0bd669958a46205f10398a371f8e6c91ddacb9f0e1943f9f7d042fb6de7957a8
2019-09-10 10:53:59 +10:00

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)