613247fc97
The approach from65f3fa8d1
worked for signing on 10.9.4, but not newer versions. 10.9.5 (and up) want each framework to stand alone. Now in addition to copying the plist's from Qt for each framework, we put them in per-version dirs and only symlink to the latest, rather than using symlinks for any contents. Rebased-From:af0bd5e
855 lines
34 KiB
Python
Executable File
855 lines
34 KiB
Python
Executable File
#!/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
|
|
from string import Template
|
|
from time import sleep
|
|
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:
|
|
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)
|
|
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)
|
|
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)
|
|
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"):
|
|
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":
|
|
# 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
|
|
|
|
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
|
|
|
|
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("-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]
|
|
|
|
# ------------------------------------------------
|
|
|
|
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)
|
|
|
|
if verbose >= 3:
|
|
print "Fancy: Importing appscript..."
|
|
try:
|
|
import appscript
|
|
except ImportError:
|
|
if verbose >= 1:
|
|
sys.stderr.write("Error: Could not import appscript which is required for fancy disk images.\n")
|
|
sys.stderr.write("Please install it e.g. with \"sudo easy_install appscript\".\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)
|
|
|
|
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(ret)
|
|
|
|
# ------------------------------------------------
|
|
|
|
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(ret)
|
|
|
|
# ------------------------------------------------
|
|
|
|
if len(config.add_qt_tr) == 0:
|
|
add_qt_tr = []
|
|
else:
|
|
qt_tr_dir = os.path.join(deploymentInfo.qtPath, "translations")
|
|
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)
|
|
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.1)
|
|
|
|
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, os.path.basename(fancy["background_picture"]))
|
|
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 "$bgpic"
|
|
do shell script "SetFile -a V /Volumes/$disk/$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 +"
|
|
|
|
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)
|