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