You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
883 lines
36 KiB
883 lines
36 KiB
#!/usr/bin/env python |
|
|
|
# |
|
# 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 |
|
|
|
# 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: %s |
|
Framework directory: %s |
|
Framework path: %s |
|
Binary name: %s |
|
Binary directory: %s |
|
Binary path: %s |
|
Version: %s |
|
Install name: %s |
|
Deployed install name: %s |
|
Source file Path: %s |
|
Deployed Directory (relative to bundle): %s |
|
""" % (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): |
|
# 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.destinationContentsDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Contents") |
|
info.destinationVersionContentsDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Versions", info.version, "Contents") |
|
|
|
return info |
|
|
|
class ApplicationBundleInfo(object): |
|
def __init__(self, path): |
|
self.path = path |
|
appName = os.path.splitext(os.path.basename(path))[0] |
|
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): |
|
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 |
|
elif os.path.exists(os.path.join(parentDir, "share", "qt4", "translations")): |
|
# MacPorts layout, e.g. "/opt/local/share/qt4" |
|
self.qtPath = os.path.join(parentDir, "share", "qt4") |
|
elif os.path.exists(os.path.join(os.path.dirname(parentDir), "share", "qt4", "translations")): |
|
# Newer Macports layout |
|
self.qtPath = os.path.join(os.path.dirname(parentDir), "share", "qt4") |
|
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): |
|
nameDot = "%s." % name |
|
libNameDot = "lib%s." % 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, verbose): |
|
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) |
|
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 %d" % 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, *args): |
|
installnametoolbin=os.getenv("INSTALLNAMETOOL", "install_name_tool") |
|
subprocess.check_call([installnametoolbin, "-"+action] + list(args)) |
|
|
|
def changeInstallName(oldName, newName, binaryPath, verbose): |
|
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, binaryPath, verbose): |
|
if verbose >= 3: |
|
print "Using install_name_tool:" |
|
print " change identification in", binaryPath |
|
print " to", id |
|
runInstallNameTool("id", id, binaryPath) |
|
|
|
def runStrip(binaryPath, verbose): |
|
stripbin=os.getenv("STRIP", "strip") |
|
if verbose >= 3: |
|
print "Using strip:" |
|
print " stripped", binaryPath |
|
subprocess.check_call([stripbin, "-x", binaryPath]) |
|
|
|
def copyFramework(framework, path, verbose): |
|
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) |
|
contentslinkfrom = os.path.join(path, framework.destinationContentsDirectory) |
|
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, bundlePath, binaryPath, strip, verbose, deploymentInfo=None): |
|
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 farmework 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, strip, verbose): |
|
frameworks = getFrameworks(applicationBundle.binaryPath, verbose) |
|
if len(frameworks) == 0 and verbose >= 1: |
|
print "Warning: Could not find any external frameworks to deploy in %s." % (applicationBundle.path) |
|
return DeploymentInfo() |
|
else: |
|
return deployFrameworks(frameworks, applicationBundle.path, applicationBundle.binaryPath, strip, verbose) |
|
|
|
def deployPlugins(appBundleInfo, deploymentInfo, strip, verbose): |
|
# 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 == "phonon" or pluginDirectory == "phonon_backend": |
|
# Deploy the phonon plugins only if phonon is in use |
|
if not deploymentInfo.usesFramework("phonon"): |
|
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 |
|
|
|
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 |
|
|
|
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 ressources; 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") |
|
|
|
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 \"%s\"\n" % (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 \"%s\"\n" % (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 \"%s\"\n" % (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 \"%s\"..." % p |
|
if not os.path.exists(p): |
|
if verbose >= 1: |
|
sys.stderr.write("Error: Could not find fancy disk image plist at \"%s\"\n" % (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 \"%s\"\n" % (p)) |
|
sys.exit(1) |
|
|
|
try: |
|
assert not fancy.has_key("window_bounds") or (isinstance(fancy["window_bounds"], list) and len(fancy["window_bounds"]) == 4) |
|
assert not fancy.has_key("background_picture") or isinstance(fancy["background_picture"], str) |
|
assert not fancy.has_key("icon_size") or isinstance(fancy["icon_size"], int) |
|
assert not fancy.has_key("applications_symlink") or isinstance(fancy["applications_symlink"], bool) |
|
if fancy.has_key("items_position"): |
|
assert isinstance(fancy["items_position"], dict) |
|
for key, value in fancy["items_position"].iteritems(): |
|
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 \"%s\"\n" % (p)) |
|
sys.exit(1) |
|
|
|
if fancy.has_key("background_picture"): |
|
bp = fancy["background_picture"] |
|
if verbose >= 3: |
|
print "Fancy: Resolving background picture \"%s\"..." % 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 \"%s\" or \"%s\"\n" % (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") |
|
|
|
# ------------------------------------------------ |
|
|
|
target = os.path.join("dist", app_bundle) |
|
|
|
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: %s\n" % 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: %s\n" % 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_%s.qm" % 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 \"%s\"..." % p |
|
if not os.path.exists(p): |
|
if verbose >= 1: |
|
sys.stderr.write("Error: Could not find Qt translation file \"%s\"\n" % (lng_file)) |
|
sys.exit(1) |
|
|
|
# ------------------------------------------------ |
|
|
|
if verbose >= 2: |
|
print "+ Installing qt.conf +" |
|
|
|
f = open(os.path.join(applicationBundle.resourcesPath, "qt.conf"), "wb") |
|
f.write(qt_conf) |
|
f.close() |
|
|
|
# ------------------------------------------------ |
|
|
|
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 %s"%(target,) |
|
subprocess.check_call("codesign --force %s %s"%(os.environ['CODESIGNARGS'], target), shell=True) |
|
|
|
# ------------------------------------------------ |
|
|
|
if config.dmg is not None: |
|
|
|
#Patch in check_output for Python 2.6 |
|
if "check_output" not in dir( subprocess ): |
|
def f(*popenargs, **kwargs): |
|
if 'stdout' in kwargs: |
|
raise ValueError('stdout argument not allowed, it will be overridden.') |
|
process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) |
|
output, unused_err = process.communicate() |
|
retcode = process.poll() |
|
if retcode: |
|
cmd = kwargs.get("args") |
|
if cmd is None: |
|
cmd = popenargs[0] |
|
raise CalledProcessError(retcode, cmd) |
|
return output |
|
subprocess.check_output = f |
|
|
|
def runHDIUtil(verb, image_basename, **kwargs): |
|
hdiutil_args = ["hdiutil", verb, image_basename + ".dmg"] |
|
if kwargs.has_key("capture_stdout"): |
|
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.iteritems(): |
|
hdiutil_args.append("-" + key) |
|
if not value is True: |
|
hdiutil_args.append(str(value)) |
|
|
|
return run(hdiutil_args) |
|
|
|
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=app_bundle_name, 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=app_bundle_name, 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("/Volumes/(.+$)", output) |
|
disk_root = m.group(0) |
|
disk_name = m.group(1) |
|
|
|
if verbose >= 2: |
|
print "+ Applying fancy settings +" |
|
|
|
if fancy.has_key("background_picture"): |
|
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 fancy.has_key("items_position"): |
|
for name, position in fancy["items_position"].iteritems(): |
|
params = { "item" : name, "position" : ",".join([str(p) for p in position]) } |
|
items_positions.append(itemscript.substitute(params)) |
|
|
|
params = { |
|
"disk" : "Bitcoin-Qt", |
|
"window_bounds" : "300,300,800,620", |
|
"icon_size" : "96", |
|
"background_commands" : "", |
|
"items_positions" : "\n ".join(items_positions) |
|
} |
|
if fancy.has_key("window_bounds"): |
|
params["window.bounds"] = ",".join([str(p) for p in fancy["window_bounds"]]) |
|
if fancy.has_key("icon_size"): |
|
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) |
|
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)
|
|
|