#!/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, os.path
from time import sleep
from argparse import ArgumentParser

qt_conf="""[Paths]
translations=Resources
plugins=PlugIns
"""

ap = ArgumentParser(description="""Front-end to macdeployqt with some additional functions.

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.""")

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("-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.add_qt_tr) == 0:
    add_qt_tr = []
else:
    qt_tr_dir = os.path.join(os.getenv("QTDIR", ""), "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 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)
target_res = os.path.join(target, "Contents", "Resources")

if verbose >= 2:
    print "+ Copying source bundle +"
if verbose >= 3:
    print app_bundle, "->", target

os.mkdir("dist")
shutil.copytree(app_bundle, target)

# ------------------------------------------------

macdeployqt_args = ["macdeployqt", target, "-verbose=%d" % verbose]
if not config.plugins:
    macdeployqt_args.append("-no-plugins")
if not config.strip:
    macdeployqt_args.append("-no-strip")

if verbose >= 2:
    print "+ Running macdeployqt +"

ret = subprocess.call(macdeployqt_args)
if ret != 0:
    sys.exit(ret)

# ------------------------------------------------

if verbose >= 2:
    print "+ Installing qt.conf +"

f = open(os.path.join(target_res, "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(target_res, lng_file)
    shutil.copy2(os.path.join(qt_tr_dir, lng_file), os.path.join(target_res, 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(target_res, 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.dmg is not None:
    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"))
        
        finder = appscript.app("Finder")
        disk = finder.disks[disk_name]
        disk.open()
        window = disk.container_window
        window.current_view.set(appscript.k.icon_view)
        window.toolbar_visible.set(False)
        window.statusbar_visible.set(False)
        if fancy.has_key("window_bounds"):
            window.bounds.set(fancy["window_bounds"])
        view_options = window.icon_view_options
        view_options.arrangement.set(appscript.k.not_arranged)
        if fancy.has_key("icon_size"):
            view_options.icon_size.set(fancy["icon_size"])
        if bg_path is not None:
            view_options.background_picture.set(disk.files[os.path.basename(bg_path)])
        if fancy.has_key("items_position"):
            for name, position in fancy["items_position"].iteritems():
                window.items[name].position.set(position)
        disk.close()
        if bg_path is not None:
            subprocess.call(["SetFile", "-a", "V", bg_path])
        disk.update(registering_applications=False)
        sleep(2)
        disk.eject()
        
        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)
