1#!/usr/bin/env python3 2# 3# Copyright (C) 2011 Patrick "p2k" Schneider <me@p2k-network.org> 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17# 18 19import subprocess, sys, re, os, shutil, stat, os.path, time 20from string import Template 21from argparse import ArgumentParser 22 23# This is ported from the original macdeployqt with modifications 24 25class FrameworkInfo(object): 26 def __init__(self): 27 self.frameworkDirectory = "" 28 self.frameworkName = "" 29 self.frameworkPath = "" 30 self.binaryDirectory = "" 31 self.binaryName = "" 32 self.binaryPath = "" 33 self.version = "" 34 self.installName = "" 35 self.deployedInstallName = "" 36 self.sourceFilePath = "" 37 self.destinationDirectory = "" 38 self.sourceResourcesDirectory = "" 39 self.sourceVersionContentsDirectory = "" 40 self.sourceContentsDirectory = "" 41 self.destinationResourcesDirectory = "" 42 self.destinationVersionContentsDirectory = "" 43 44 def __eq__(self, other): 45 if self.__class__ == other.__class__: 46 return self.__dict__ == other.__dict__ 47 else: 48 return False 49 50 def __str__(self): 51 return """ Framework name: %s 52 Framework directory: %s 53 Framework path: %s 54 Binary name: %s 55 Binary directory: %s 56 Binary path: %s 57 Version: %s 58 Install name: %s 59 Deployed install name: %s 60 Source file Path: %s 61 Deployed Directory (relative to bundle): %s 62""" % (self.frameworkName, 63 self.frameworkDirectory, 64 self.frameworkPath, 65 self.binaryName, 66 self.binaryDirectory, 67 self.binaryPath, 68 self.version, 69 self.installName, 70 self.deployedInstallName, 71 self.sourceFilePath, 72 self.destinationDirectory) 73 74 def isDylib(self): 75 return self.frameworkName.endswith(".dylib") 76 77 def isQtFramework(self): 78 if self.isDylib(): 79 return self.frameworkName.startswith("libQt") 80 else: 81 return self.frameworkName.startswith("Qt") 82 83 reOLine = re.compile(r'^(.+) \(compatibility version [0-9.]+, current version [0-9.]+\)$') 84 bundleFrameworkDirectory = "Contents/Frameworks" 85 bundleBinaryDirectory = "Contents/MacOS" 86 87 @classmethod 88 def fromOtoolLibraryLine(cls, line): 89 # Note: line must be trimmed 90 if line == "": 91 return None 92 93 # Don't deploy system libraries (exception for libQtuitools and libQtlucene). 94 if line.startswith("/System/Library/") or line.startswith("@executable_path") or (line.startswith("/usr/lib/") and "libQt" not in line): 95 return None 96 97 m = cls.reOLine.match(line) 98 if m is None: 99 raise RuntimeError("otool line could not be parsed: " + line) 100 101 path = m.group(1) 102 103 info = cls() 104 info.sourceFilePath = path 105 info.installName = path 106 107 if path.endswith(".dylib"): 108 dirname, filename = os.path.split(path) 109 info.frameworkName = filename 110 info.frameworkDirectory = dirname 111 info.frameworkPath = path 112 113 info.binaryDirectory = dirname 114 info.binaryName = filename 115 info.binaryPath = path 116 info.version = "-" 117 118 info.installName = path 119 info.deployedInstallName = "@executable_path/../Frameworks/" + info.binaryName 120 info.sourceFilePath = path 121 info.destinationDirectory = cls.bundleFrameworkDirectory 122 else: 123 parts = path.split("/") 124 i = 0 125 # Search for the .framework directory 126 for part in parts: 127 if part.endswith(".framework"): 128 break 129 i += 1 130 if i == len(parts): 131 raise RuntimeError("Could not find .framework or .dylib in otool line: " + line) 132 133 info.frameworkName = parts[i] 134 info.frameworkDirectory = "/".join(parts[:i]) 135 info.frameworkPath = os.path.join(info.frameworkDirectory, info.frameworkName) 136 137 info.binaryName = parts[i+3] 138 info.binaryDirectory = "/".join(parts[i+1:i+3]) 139 info.binaryPath = os.path.join(info.binaryDirectory, info.binaryName) 140 info.version = parts[i+2] 141 142 info.deployedInstallName = "@executable_path/../Frameworks/" + os.path.join(info.frameworkName, info.binaryPath) 143 info.destinationDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, info.binaryDirectory) 144 145 info.sourceResourcesDirectory = os.path.join(info.frameworkPath, "Resources") 146 info.sourceContentsDirectory = os.path.join(info.frameworkPath, "Contents") 147 info.sourceVersionContentsDirectory = os.path.join(info.frameworkPath, "Versions", info.version, "Contents") 148 info.destinationResourcesDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Resources") 149 info.destinationContentsDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Contents") 150 info.destinationVersionContentsDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Versions", info.version, "Contents") 151 152 return info 153 154class ApplicationBundleInfo(object): 155 def __init__(self, path): 156 self.path = path 157 appName = "Litecoin-Qt" 158 self.binaryPath = os.path.join(path, "Contents", "MacOS", appName) 159 if not os.path.exists(self.binaryPath): 160 raise RuntimeError("Could not find bundle binary for " + path) 161 self.resourcesPath = os.path.join(path, "Contents", "Resources") 162 self.pluginPath = os.path.join(path, "Contents", "PlugIns") 163 164class DeploymentInfo(object): 165 def __init__(self): 166 self.qtPath = None 167 self.pluginPath = None 168 self.deployedFrameworks = [] 169 170 def detectQtPath(self, frameworkDirectory): 171 parentDir = os.path.dirname(frameworkDirectory) 172 if os.path.exists(os.path.join(parentDir, "translations")): 173 # Classic layout, e.g. "/usr/local/Trolltech/Qt-4.x.x" 174 self.qtPath = parentDir 175 else: 176 self.qtPath = os.getenv("QTDIR", None) 177 178 if self.qtPath is not None: 179 pluginPath = os.path.join(self.qtPath, "plugins") 180 if os.path.exists(pluginPath): 181 self.pluginPath = pluginPath 182 183 def usesFramework(self, name): 184 nameDot = "%s." % name 185 libNameDot = "lib%s." % name 186 for framework in self.deployedFrameworks: 187 if framework.endswith(".framework"): 188 if framework.startswith(nameDot): 189 return True 190 elif framework.endswith(".dylib"): 191 if framework.startswith(libNameDot): 192 return True 193 return False 194 195def getFrameworks(binaryPath, verbose): 196 if verbose >= 3: 197 print("Inspecting with otool: " + binaryPath) 198 otoolbin=os.getenv("OTOOL", "otool") 199 otool = subprocess.Popen([otoolbin, "-L", binaryPath], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) 200 o_stdout, o_stderr = otool.communicate() 201 if otool.returncode != 0: 202 if verbose >= 1: 203 sys.stderr.write(o_stderr) 204 sys.stderr.flush() 205 raise RuntimeError("otool failed with return code %d" % otool.returncode) 206 207 otoolLines = o_stdout.split("\n") 208 otoolLines.pop(0) # First line is the inspected binary 209 if ".framework" in binaryPath or binaryPath.endswith(".dylib"): 210 otoolLines.pop(0) # Frameworks and dylibs list themselves as a dependency. 211 212 libraries = [] 213 for line in otoolLines: 214 line = line.replace("@loader_path", os.path.dirname(binaryPath)) 215 info = FrameworkInfo.fromOtoolLibraryLine(line.strip()) 216 if info is not None: 217 if verbose >= 3: 218 print("Found framework:") 219 print(info) 220 libraries.append(info) 221 222 return libraries 223 224def runInstallNameTool(action, *args): 225 installnametoolbin=os.getenv("INSTALLNAMETOOL", "install_name_tool") 226 subprocess.check_call([installnametoolbin, "-"+action] + list(args)) 227 228def changeInstallName(oldName, newName, binaryPath, verbose): 229 if verbose >= 3: 230 print("Using install_name_tool:") 231 print(" in", binaryPath) 232 print(" change reference", oldName) 233 print(" to", newName) 234 runInstallNameTool("change", oldName, newName, binaryPath) 235 236def changeIdentification(id, binaryPath, verbose): 237 if verbose >= 3: 238 print("Using install_name_tool:") 239 print(" change identification in", binaryPath) 240 print(" to", id) 241 runInstallNameTool("id", id, binaryPath) 242 243def runStrip(binaryPath, verbose): 244 stripbin=os.getenv("STRIP", "strip") 245 if verbose >= 3: 246 print("Using strip:") 247 print(" stripped", binaryPath) 248 subprocess.check_call([stripbin, "-x", binaryPath]) 249 250def copyFramework(framework, path, verbose): 251 if framework.sourceFilePath.startswith("Qt"): 252 #standard place for Nokia Qt installer's frameworks 253 fromPath = "/Library/Frameworks/" + framework.sourceFilePath 254 else: 255 fromPath = framework.sourceFilePath 256 toDir = os.path.join(path, framework.destinationDirectory) 257 toPath = os.path.join(toDir, framework.binaryName) 258 259 if not os.path.exists(fromPath): 260 raise RuntimeError("No file at " + fromPath) 261 262 if os.path.exists(toPath): 263 return None # Already there 264 265 if not os.path.exists(toDir): 266 os.makedirs(toDir) 267 268 shutil.copy2(fromPath, toPath) 269 if verbose >= 3: 270 print("Copied:", fromPath) 271 print(" to:", toPath) 272 273 permissions = os.stat(toPath) 274 if not permissions.st_mode & stat.S_IWRITE: 275 os.chmod(toPath, permissions.st_mode | stat.S_IWRITE) 276 277 if not framework.isDylib(): # Copy resources for real frameworks 278 279 linkfrom = os.path.join(path, "Contents","Frameworks", framework.frameworkName, "Versions", "Current") 280 linkto = framework.version 281 if not os.path.exists(linkfrom): 282 os.symlink(linkto, linkfrom) 283 if verbose >= 2: 284 print("Linked:", linkfrom, "->", linkto) 285 fromResourcesDir = framework.sourceResourcesDirectory 286 if os.path.exists(fromResourcesDir): 287 toResourcesDir = os.path.join(path, framework.destinationResourcesDirectory) 288 shutil.copytree(fromResourcesDir, toResourcesDir, symlinks=True) 289 if verbose >= 3: 290 print("Copied resources:", fromResourcesDir) 291 print(" to:", toResourcesDir) 292 fromContentsDir = framework.sourceVersionContentsDirectory 293 if not os.path.exists(fromContentsDir): 294 fromContentsDir = framework.sourceContentsDirectory 295 if os.path.exists(fromContentsDir): 296 toContentsDir = os.path.join(path, framework.destinationVersionContentsDirectory) 297 shutil.copytree(fromContentsDir, toContentsDir, symlinks=True) 298 if verbose >= 3: 299 print("Copied Contents:", fromContentsDir) 300 print(" to:", toContentsDir) 301 elif framework.frameworkName.startswith("libQtGui"): # Copy qt_menu.nib (applies to non-framework layout) 302 qtMenuNibSourcePath = os.path.join(framework.frameworkDirectory, "Resources", "qt_menu.nib") 303 qtMenuNibDestinationPath = os.path.join(path, "Contents", "Resources", "qt_menu.nib") 304 if os.path.exists(qtMenuNibSourcePath) and not os.path.exists(qtMenuNibDestinationPath): 305 shutil.copytree(qtMenuNibSourcePath, qtMenuNibDestinationPath, symlinks=True) 306 if verbose >= 3: 307 print("Copied for libQtGui:", qtMenuNibSourcePath) 308 print(" to:", qtMenuNibDestinationPath) 309 310 return toPath 311 312def deployFrameworks(frameworks, bundlePath, binaryPath, strip, verbose, deploymentInfo=None): 313 if deploymentInfo is None: 314 deploymentInfo = DeploymentInfo() 315 316 while len(frameworks) > 0: 317 framework = frameworks.pop(0) 318 deploymentInfo.deployedFrameworks.append(framework.frameworkName) 319 320 if verbose >= 2: 321 print("Processing", framework.frameworkName, "...") 322 323 # Get the Qt path from one of the Qt frameworks 324 if deploymentInfo.qtPath is None and framework.isQtFramework(): 325 deploymentInfo.detectQtPath(framework.frameworkDirectory) 326 327 if framework.installName.startswith("@executable_path") or framework.installName.startswith(bundlePath): 328 if verbose >= 2: 329 print(framework.frameworkName, "already deployed, skipping.") 330 continue 331 332 # install_name_tool the new id into the binary 333 changeInstallName(framework.installName, framework.deployedInstallName, binaryPath, verbose) 334 335 # Copy framework to app bundle. 336 deployedBinaryPath = copyFramework(framework, bundlePath, verbose) 337 # Skip the rest if already was deployed. 338 if deployedBinaryPath is None: 339 continue 340 341 if strip: 342 runStrip(deployedBinaryPath, verbose) 343 344 # install_name_tool it a new id. 345 changeIdentification(framework.deployedInstallName, deployedBinaryPath, verbose) 346 # Check for framework dependencies 347 dependencies = getFrameworks(deployedBinaryPath, verbose) 348 349 for dependency in dependencies: 350 changeInstallName(dependency.installName, dependency.deployedInstallName, deployedBinaryPath, verbose) 351 352 # Deploy framework if necessary. 353 if dependency.frameworkName not in deploymentInfo.deployedFrameworks and dependency not in frameworks: 354 frameworks.append(dependency) 355 356 return deploymentInfo 357 358def deployFrameworksForAppBundle(applicationBundle, strip, verbose): 359 frameworks = getFrameworks(applicationBundle.binaryPath, verbose) 360 if len(frameworks) == 0 and verbose >= 1: 361 print("Warning: Could not find any external frameworks to deploy in %s." % (applicationBundle.path)) 362 return DeploymentInfo() 363 else: 364 return deployFrameworks(frameworks, applicationBundle.path, applicationBundle.binaryPath, strip, verbose) 365 366def deployPlugins(appBundleInfo, deploymentInfo, strip, verbose): 367 # Lookup available plugins, exclude unneeded 368 plugins = [] 369 if deploymentInfo.pluginPath is None: 370 return 371 for dirpath, dirnames, filenames in os.walk(deploymentInfo.pluginPath): 372 pluginDirectory = os.path.relpath(dirpath, deploymentInfo.pluginPath) 373 if pluginDirectory == "designer": 374 # Skip designer plugins 375 continue 376 elif pluginDirectory == "phonon" or pluginDirectory == "phonon_backend": 377 # Deploy the phonon plugins only if phonon is in use 378 if not deploymentInfo.usesFramework("phonon"): 379 continue 380 elif pluginDirectory == "sqldrivers": 381 # Deploy the sql plugins only if QtSql is in use 382 if not deploymentInfo.usesFramework("QtSql"): 383 continue 384 elif pluginDirectory == "script": 385 # Deploy the script plugins only if QtScript is in use 386 if not deploymentInfo.usesFramework("QtScript"): 387 continue 388 elif pluginDirectory == "qmltooling" or pluginDirectory == "qml1tooling": 389 # Deploy the qml plugins only if QtDeclarative is in use 390 if not deploymentInfo.usesFramework("QtDeclarative"): 391 continue 392 elif pluginDirectory == "bearer": 393 # Deploy the bearer plugins only if QtNetwork is in use 394 if not deploymentInfo.usesFramework("QtNetwork"): 395 continue 396 elif pluginDirectory == "position": 397 # Deploy the position plugins only if QtPositioning is in use 398 if not deploymentInfo.usesFramework("QtPositioning"): 399 continue 400 elif pluginDirectory == "sensors" or pluginDirectory == "sensorgestures": 401 # Deploy the sensor plugins only if QtSensors is in use 402 if not deploymentInfo.usesFramework("QtSensors"): 403 continue 404 elif pluginDirectory == "audio" or pluginDirectory == "playlistformats": 405 # Deploy the audio plugins only if QtMultimedia is in use 406 if not deploymentInfo.usesFramework("QtMultimedia"): 407 continue 408 elif pluginDirectory == "mediaservice": 409 # Deploy the mediaservice plugins only if QtMultimediaWidgets is in use 410 if not deploymentInfo.usesFramework("QtMultimediaWidgets"): 411 continue 412 413 for pluginName in filenames: 414 pluginPath = os.path.join(pluginDirectory, pluginName) 415 if pluginName.endswith("_debug.dylib"): 416 # Skip debug plugins 417 continue 418 elif pluginPath == "imageformats/libqsvg.dylib" or pluginPath == "iconengines/libqsvgicon.dylib": 419 # Deploy the svg plugins only if QtSvg is in use 420 if not deploymentInfo.usesFramework("QtSvg"): 421 continue 422 elif pluginPath == "accessible/libqtaccessiblecompatwidgets.dylib": 423 # Deploy accessibility for Qt3Support only if the Qt3Support is in use 424 if not deploymentInfo.usesFramework("Qt3Support"): 425 continue 426 elif pluginPath == "graphicssystems/libqglgraphicssystem.dylib": 427 # Deploy the opengl graphicssystem plugin only if QtOpenGL is in use 428 if not deploymentInfo.usesFramework("QtOpenGL"): 429 continue 430 elif pluginPath == "accessible/libqtaccessiblequick.dylib": 431 # Deploy the accessible qtquick plugin only if QtQuick is in use 432 if not deploymentInfo.usesFramework("QtQuick"): 433 continue 434 435 plugins.append((pluginDirectory, pluginName)) 436 437 for pluginDirectory, pluginName in plugins: 438 if verbose >= 2: 439 print("Processing plugin", os.path.join(pluginDirectory, pluginName), "...") 440 441 sourcePath = os.path.join(deploymentInfo.pluginPath, pluginDirectory, pluginName) 442 destinationDirectory = os.path.join(appBundleInfo.pluginPath, pluginDirectory) 443 if not os.path.exists(destinationDirectory): 444 os.makedirs(destinationDirectory) 445 446 destinationPath = os.path.join(destinationDirectory, pluginName) 447 shutil.copy2(sourcePath, destinationPath) 448 if verbose >= 3: 449 print("Copied:", sourcePath) 450 print(" to:", destinationPath) 451 452 if strip: 453 runStrip(destinationPath, verbose) 454 455 dependencies = getFrameworks(destinationPath, verbose) 456 457 for dependency in dependencies: 458 changeInstallName(dependency.installName, dependency.deployedInstallName, destinationPath, verbose) 459 460 # Deploy framework if necessary. 461 if dependency.frameworkName not in deploymentInfo.deployedFrameworks: 462 deployFrameworks([dependency], appBundleInfo.path, destinationPath, strip, verbose, deploymentInfo) 463 464qt_conf="""[Paths] 465Translations=Resources 466Plugins=PlugIns 467""" 468 469ap = ArgumentParser(description="""Improved version of macdeployqt. 470 471Outputs a ready-to-deploy app in a folder "dist" and optionally wraps it in a .dmg file. 472Note, that the "dist" folder will be deleted before deploying on each run. 473 474Optionally, Qt translation files (.qm) and additional resources can be added to the bundle. 475 476Also optionally signs the .app bundle; set the CODESIGNARGS environment variable to pass arguments 477to the codesign tool. 478E.g. CODESIGNARGS='--sign "Developer ID Application: ..." --keychain /encrypted/foo.keychain'""") 479 480ap.add_argument("app_bundle", nargs=1, metavar="app-bundle", help="application bundle to be deployed") 481ap.add_argument("-verbose", type=int, nargs=1, default=[1], metavar="<0-3>", help="0 = no output, 1 = error/warning (default), 2 = normal, 3 = debug") 482ap.add_argument("-no-plugins", dest="plugins", action="store_false", default=True, help="skip plugin deployment") 483ap.add_argument("-no-strip", dest="strip", action="store_false", default=True, help="don't run 'strip' on the binaries") 484ap.add_argument("-sign", dest="sign", action="store_true", default=False, help="sign .app bundle with codesign tool") 485ap.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") 486ap.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") 487ap.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") 488ap.add_argument("-translations-dir", nargs=1, metavar="path", default=None, help="Path to Qt's translation files") 489ap.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") 490ap.add_argument("-volname", nargs=1, metavar="volname", default=[], help="custom volume name for dmg") 491 492config = ap.parse_args() 493 494verbose = config.verbose[0] 495 496# ------------------------------------------------ 497 498app_bundle = config.app_bundle[0] 499 500if not os.path.exists(app_bundle): 501 if verbose >= 1: 502 sys.stderr.write("Error: Could not find app bundle \"%s\"\n" % (app_bundle)) 503 sys.exit(1) 504 505app_bundle_name = os.path.splitext(os.path.basename(app_bundle))[0] 506 507# ------------------------------------------------ 508translations_dir = None 509if config.translations_dir and config.translations_dir[0]: 510 if os.path.exists(config.translations_dir[0]): 511 translations_dir = config.translations_dir[0] 512 else: 513 if verbose >= 1: 514 sys.stderr.write("Error: Could not find translation dir \"%s\"\n" % (translations_dir)) 515 sys.exit(1) 516# ------------------------------------------------ 517 518for p in config.add_resources: 519 if verbose >= 3: 520 print("Checking for \"%s\"..." % p) 521 if not os.path.exists(p): 522 if verbose >= 1: 523 sys.stderr.write("Error: Could not find additional resource file \"%s\"\n" % (p)) 524 sys.exit(1) 525 526# ------------------------------------------------ 527 528if len(config.fancy) == 1: 529 if verbose >= 3: 530 print("Fancy: Importing plistlib...") 531 try: 532 import plistlib 533 except ImportError: 534 if verbose >= 1: 535 sys.stderr.write("Error: Could not import plistlib which is required for fancy disk images.\n") 536 sys.exit(1) 537 538 p = config.fancy[0] 539 if verbose >= 3: 540 print("Fancy: Loading \"%s\"..." % p) 541 if not os.path.exists(p): 542 if verbose >= 1: 543 sys.stderr.write("Error: Could not find fancy disk image plist at \"%s\"\n" % (p)) 544 sys.exit(1) 545 546 try: 547 fancy = plistlib.readPlist(p) 548 except: 549 if verbose >= 1: 550 sys.stderr.write("Error: Could not parse fancy disk image plist at \"%s\"\n" % (p)) 551 sys.exit(1) 552 553 try: 554 assert "window_bounds" not in fancy or (isinstance(fancy["window_bounds"], list) and len(fancy["window_bounds"]) == 4) 555 assert "background_picture" not in fancy or isinstance(fancy["background_picture"], str) 556 assert "icon_size" not in fancy or isinstance(fancy["icon_size"], int) 557 assert "applications_symlink" not in fancy or isinstance(fancy["applications_symlink"], bool) 558 if "items_position" in fancy: 559 assert isinstance(fancy["items_position"], dict) 560 for key, value in fancy["items_position"].items(): 561 assert isinstance(value, list) and len(value) == 2 and isinstance(value[0], int) and isinstance(value[1], int) 562 except: 563 if verbose >= 1: 564 sys.stderr.write("Error: Bad format of fancy disk image plist at \"%s\"\n" % (p)) 565 sys.exit(1) 566 567 if "background_picture" in fancy: 568 bp = fancy["background_picture"] 569 if verbose >= 3: 570 print("Fancy: Resolving background picture \"%s\"..." % bp) 571 if not os.path.exists(bp): 572 bp = os.path.join(os.path.dirname(p), bp) 573 if not os.path.exists(bp): 574 if verbose >= 1: 575 sys.stderr.write("Error: Could not find background picture at \"%s\" or \"%s\"\n" % (fancy["background_picture"], bp)) 576 sys.exit(1) 577 else: 578 fancy["background_picture"] = bp 579else: 580 fancy = None 581 582# ------------------------------------------------ 583 584if os.path.exists("dist"): 585 if verbose >= 2: 586 print("+ Removing old dist folder +") 587 588 shutil.rmtree("dist") 589 590# ------------------------------------------------ 591 592if len(config.volname) == 1: 593 volname = config.volname[0] 594else: 595 volname = app_bundle_name 596 597# ------------------------------------------------ 598 599target = os.path.join("dist", "Litecoin-Qt.app") 600 601if verbose >= 2: 602 print("+ Copying source bundle +") 603if verbose >= 3: 604 print(app_bundle, "->", target) 605 606os.mkdir("dist") 607shutil.copytree(app_bundle, target, symlinks=True) 608 609applicationBundle = ApplicationBundleInfo(target) 610 611# ------------------------------------------------ 612 613if verbose >= 2: 614 print("+ Deploying frameworks +") 615 616try: 617 deploymentInfo = deployFrameworksForAppBundle(applicationBundle, config.strip, verbose) 618 if deploymentInfo.qtPath is None: 619 deploymentInfo.qtPath = os.getenv("QTDIR", None) 620 if deploymentInfo.qtPath is None: 621 if verbose >= 1: 622 sys.stderr.write("Warning: Could not detect Qt's path, skipping plugin deployment!\n") 623 config.plugins = False 624except RuntimeError as e: 625 if verbose >= 1: 626 sys.stderr.write("Error: %s\n" % str(e)) 627 sys.exit(1) 628 629# ------------------------------------------------ 630 631if config.plugins: 632 if verbose >= 2: 633 print("+ Deploying plugins +") 634 635 try: 636 deployPlugins(applicationBundle, deploymentInfo, config.strip, verbose) 637 except RuntimeError as e: 638 if verbose >= 1: 639 sys.stderr.write("Error: %s\n" % str(e)) 640 sys.exit(1) 641 642# ------------------------------------------------ 643 644if len(config.add_qt_tr) == 0: 645 add_qt_tr = [] 646else: 647 if translations_dir is not None: 648 qt_tr_dir = translations_dir 649 else: 650 if deploymentInfo.qtPath is not None: 651 qt_tr_dir = os.path.join(deploymentInfo.qtPath, "translations") 652 else: 653 sys.stderr.write("Error: Could not find Qt translation path\n") 654 sys.exit(1) 655 add_qt_tr = ["qt_%s.qm" % lng for lng in config.add_qt_tr[0].split(",")] 656 for lng_file in add_qt_tr: 657 p = os.path.join(qt_tr_dir, lng_file) 658 if verbose >= 3: 659 print("Checking for \"%s\"..." % p) 660 if not os.path.exists(p): 661 if verbose >= 1: 662 sys.stderr.write("Error: Could not find Qt translation file \"%s\"\n" % (lng_file)) 663 sys.exit(1) 664 665# ------------------------------------------------ 666 667if verbose >= 2: 668 print("+ Installing qt.conf +") 669 670with open(os.path.join(applicationBundle.resourcesPath, "qt.conf"), "wb") as f: 671 f.write(qt_conf.encode()) 672 673# ------------------------------------------------ 674 675if len(add_qt_tr) > 0 and verbose >= 2: 676 print("+ Adding Qt translations +") 677 678for lng_file in add_qt_tr: 679 if verbose >= 3: 680 print(os.path.join(qt_tr_dir, lng_file), "->", os.path.join(applicationBundle.resourcesPath, lng_file)) 681 shutil.copy2(os.path.join(qt_tr_dir, lng_file), os.path.join(applicationBundle.resourcesPath, lng_file)) 682 683# ------------------------------------------------ 684 685if len(config.add_resources) > 0 and verbose >= 2: 686 print("+ Adding additional resources +") 687 688for p in config.add_resources: 689 t = os.path.join(applicationBundle.resourcesPath, os.path.basename(p)) 690 if verbose >= 3: 691 print(p, "->", t) 692 if os.path.isdir(p): 693 shutil.copytree(p, t, symlinks=True) 694 else: 695 shutil.copy2(p, t) 696 697# ------------------------------------------------ 698 699if config.sign and 'CODESIGNARGS' not in os.environ: 700 print("You must set the CODESIGNARGS environment variable. Skipping signing.") 701elif config.sign: 702 if verbose >= 1: 703 print("Code-signing app bundle %s"%(target,)) 704 subprocess.check_call("codesign --force %s %s"%(os.environ['CODESIGNARGS'], target), shell=True) 705 706# ------------------------------------------------ 707 708if config.dmg is not None: 709 710 def runHDIUtil(verb, image_basename, **kwargs): 711 hdiutil_args = ["hdiutil", verb, image_basename + ".dmg"] 712 if "capture_stdout" in kwargs: 713 del kwargs["capture_stdout"] 714 run = subprocess.check_output 715 else: 716 if verbose < 2: 717 hdiutil_args.append("-quiet") 718 elif verbose >= 3: 719 hdiutil_args.append("-verbose") 720 run = subprocess.check_call 721 722 for key, value in kwargs.items(): 723 hdiutil_args.append("-" + key) 724 if not value is True: 725 hdiutil_args.append(str(value)) 726 727 return run(hdiutil_args, universal_newlines=True) 728 729 if verbose >= 2: 730 if fancy is None: 731 print("+ Creating .dmg disk image +") 732 else: 733 print("+ Preparing .dmg disk image +") 734 735 if config.dmg != "": 736 dmg_name = config.dmg 737 else: 738 spl = app_bundle_name.split(" ") 739 dmg_name = spl[0] + "".join(p.capitalize() for p in spl[1:]) 740 741 if fancy is None: 742 try: 743 runHDIUtil("create", dmg_name, srcfolder="dist", format="UDBZ", volname=volname, ov=True) 744 except subprocess.CalledProcessError as e: 745 sys.exit(e.returncode) 746 else: 747 if verbose >= 3: 748 print("Determining size of \"dist\"...") 749 size = 0 750 for path, dirs, files in os.walk("dist"): 751 for file in files: 752 size += os.path.getsize(os.path.join(path, file)) 753 size += int(size * 0.15) 754 755 if verbose >= 3: 756 print("Creating temp image for modification...") 757 try: 758 runHDIUtil("create", dmg_name + ".temp", srcfolder="dist", format="UDRW", size=size, volname=volname, ov=True) 759 except subprocess.CalledProcessError as e: 760 sys.exit(e.returncode) 761 762 if verbose >= 3: 763 print("Attaching temp image...") 764 try: 765 output = runHDIUtil("attach", dmg_name + ".temp", readwrite=True, noverify=True, noautoopen=True, capture_stdout=True) 766 except subprocess.CalledProcessError as e: 767 sys.exit(e.returncode) 768 769 m = re.search("/Volumes/(.+$)", output) 770 disk_root = m.group(0) 771 disk_name = m.group(1) 772 773 if verbose >= 2: 774 print("+ Applying fancy settings +") 775 776 if "background_picture" in fancy: 777 bg_path = os.path.join(disk_root, ".background", os.path.basename(fancy["background_picture"])) 778 os.mkdir(os.path.dirname(bg_path)) 779 if verbose >= 3: 780 print(fancy["background_picture"], "->", bg_path) 781 shutil.copy2(fancy["background_picture"], bg_path) 782 else: 783 bg_path = None 784 785 if fancy.get("applications_symlink", False): 786 os.symlink("/Applications", os.path.join(disk_root, "Applications")) 787 788 # The Python appscript package broke with OSX 10.8 and isn't being fixed. 789 # So we now build up an AppleScript string and use the osascript command 790 # to make the .dmg file pretty: 791 appscript = Template( """ 792 on run argv 793 tell application "Finder" 794 tell disk "$disk" 795 open 796 set current view of container window to icon view 797 set toolbar visible of container window to false 798 set statusbar visible of container window to false 799 set the bounds of container window to {$window_bounds} 800 set theViewOptions to the icon view options of container window 801 set arrangement of theViewOptions to not arranged 802 set icon size of theViewOptions to $icon_size 803 $background_commands 804 $items_positions 805 close -- close/reopen works around a bug... 806 open 807 update without registering applications 808 delay 5 809 eject 810 end tell 811 end tell 812 end run 813 """) 814 815 itemscript = Template('set position of item "${item}" of container window to {${position}}') 816 items_positions = [] 817 if "items_position" in fancy: 818 for name, position in fancy["items_position"].items(): 819 params = { "item" : name, "position" : ",".join([str(p) for p in position]) } 820 items_positions.append(itemscript.substitute(params)) 821 822 params = { 823 "disk" : volname, 824 "window_bounds" : "300,300,800,620", 825 "icon_size" : "96", 826 "background_commands" : "", 827 "items_positions" : "\n ".join(items_positions) 828 } 829 if "window_bounds" in fancy: 830 params["window_bounds"] = ",".join([str(p) for p in fancy["window_bounds"]]) 831 if "icon_size" in fancy: 832 params["icon_size"] = str(fancy["icon_size"]) 833 if bg_path is not None: 834 # Set background file, then call SetFile to make it invisible. 835 # (note: making it invisible first makes set background picture fail) 836 bgscript = Template("""set background picture of theViewOptions to file ".background:$bgpic" 837 do shell script "SetFile -a V /Volumes/$disk/.background/$bgpic" """) 838 params["background_commands"] = bgscript.substitute({"bgpic" : os.path.basename(bg_path), "disk" : params["disk"]}) 839 840 s = appscript.substitute(params) 841 if verbose >= 2: 842 print("Running AppleScript:") 843 print(s) 844 845 p = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE) 846 p.communicate(input=s.encode('utf-8')) 847 if p.returncode: 848 print("Error running osascript.") 849 850 if verbose >= 2: 851 print("+ Finalizing .dmg disk image +") 852 time.sleep(5) 853 854 try: 855 runHDIUtil("convert", dmg_name + ".temp", format="UDBZ", o=dmg_name + ".dmg", ov=True) 856 except subprocess.CalledProcessError as e: 857 sys.exit(e.returncode) 858 859 os.unlink(dmg_name + ".temp.dmg") 860 861# ------------------------------------------------ 862 863if verbose >= 2: 864 print("+ Done +") 865 866sys.exit(0) 867