4 # Copyright (C) 2011 Patrick "p2k" Schneider <me@p2k-network.org>
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import subprocess, sys, re, os, shutil, os.path
21 from time import sleep
22 from argparse import ArgumentParser
25 translations=Resources
29 ap = ArgumentParser(description="""Front-end to macdeployqt with some additional functions.
31 Outputs a ready-to-deploy app in a folder "dist" and optionally wraps it in a .dmg file.
32 Note, that the "dist" folder will be deleted before deploying on each run.
34 Optionally, Qt translation files (.qm) and additional resources can be added to the bundle.""")
36 ap.add_argument("app_bundle", nargs=1, metavar="app-bundle", help="application bundle to be deployed")
37 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")
38 ap.add_argument("-no-plugins", dest="plugins", action="store_false", default=True, help="skip plugin deployment")
39 ap.add_argument("-no-strip", dest="strip", action="store_false", default=True, help="don't run 'strip' on the binaries")
40 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")
41 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")
42 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")
43 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")
45 config = ap.parse_args()
47 verbose = config.verbose[0]
49 # ------------------------------------------------
51 app_bundle = config.app_bundle[0]
53 if not os.path.exists(app_bundle):
55 sys.stderr.write("Error: Could not find app bundle \"%s\"\n" % (app_bundle))
58 app_bundle_name = os.path.splitext(os.path.basename(app_bundle))[0]
60 # ------------------------------------------------
62 for p in config.add_resources:
64 print "Checking for \"%s\"..." % p
65 if not os.path.exists(p):
67 sys.stderr.write("Error: Could not find additional resource file \"%s\"\n" % (p))
70 # ------------------------------------------------
72 if len(config.add_qt_tr) == 0:
75 qt_tr_dir = os.path.join(os.getenv("QTDIR", ""), "translations")
76 add_qt_tr = ["qt_%s.qm" % lng for lng in config.add_qt_tr[0].split(",")]
77 for lng_file in add_qt_tr:
78 p = os.path.join(qt_tr_dir, lng_file)
80 print "Checking for \"%s\"..." % p
81 if not os.path.exists(p):
83 sys.stderr.write("Error: Could not find Qt translation file \"%s\"\n" % (lng_file))
86 # ------------------------------------------------
88 if len(config.fancy) == 1:
90 print "Fancy: Importing plistlib..."
95 sys.stderr.write("Error: Could not import plistlib which is required for fancy disk images.\n")
99 print "Fancy: Importing appscript..."
104 sys.stderr.write("Error: Could not import appscript which is required for fancy disk images.\n")
105 sys.stderr.write("Please install it e.g. with \"sudo easy_install appscript\".\n")
110 print "Fancy: Loading \"%s\"..." % p
111 if not os.path.exists(p):
113 sys.stderr.write("Error: Could not find fancy disk image plist at \"%s\"\n" % (p))
117 fancy = plistlib.readPlist(p)
120 sys.stderr.write("Error: Could not parse fancy disk image plist at \"%s\"\n" % (p))
124 assert not fancy.has_key("window_bounds") or (isinstance(fancy["window_bounds"], list) and len(fancy["window_bounds"]) == 4)
125 assert not fancy.has_key("background_picture") or isinstance(fancy["background_picture"], str)
126 assert not fancy.has_key("icon_size") or isinstance(fancy["icon_size"], int)
127 assert not fancy.has_key("applications_symlink") or isinstance(fancy["applications_symlink"], bool)
128 if fancy.has_key("items_position"):
129 assert isinstance(fancy["items_position"], dict)
130 for key, value in fancy["items_position"].iteritems():
131 assert isinstance(value, list) and len(value) == 2 and isinstance(value[0], int) and isinstance(value[1], int)
134 sys.stderr.write("Error: Bad format of fancy disk image plist at \"%s\"\n" % (p))
137 if fancy.has_key("background_picture"):
138 bp = fancy["background_picture"]
140 print "Fancy: Resolving background picture \"%s\"..." % bp
141 if not os.path.exists(bp):
142 bp = os.path.join(os.path.dirname(p), bp)
143 if not os.path.exists(bp):
145 sys.stderr.write("Error: Could not find background picture at \"%s\" or \"%s\"\n" % (fancy["background_picture"], bp))
148 fancy["background_picture"] = bp
152 # ------------------------------------------------
154 if os.path.exists("dist"):
156 print "+ Removing old dist folder +"
158 shutil.rmtree("dist")
160 # ------------------------------------------------
162 target = os.path.join("dist", app_bundle)
163 target_res = os.path.join(target, "Contents", "Resources")
166 print "+ Copying source bundle +"
168 print app_bundle, "->", target
171 shutil.copytree(app_bundle, target)
173 # ------------------------------------------------
175 macdeployqt_args = ["macdeployqt", target, "-verbose=%d" % verbose]
176 if not config.plugins:
177 macdeployqt_args.append("-no-plugins")
179 macdeployqt_args.append("-no-strip")
182 print "+ Running macdeployqt +"
184 ret = subprocess.call(macdeployqt_args)
188 # ------------------------------------------------
191 print "+ Installing qt.conf +"
193 f = open(os.path.join(target_res, "qt.conf"), "wb")
197 # ------------------------------------------------
199 if len(add_qt_tr) > 0 and verbose >= 2:
200 print "+ Adding Qt translations +"
202 for lng_file in add_qt_tr:
204 print os.path.join(qt_tr_dir, lng_file), "->", os.path.join(target_res, lng_file)
205 shutil.copy2(os.path.join(qt_tr_dir, lng_file), os.path.join(target_res, lng_file))
207 # ------------------------------------------------
209 if len(config.add_resources) > 0 and verbose >= 2:
210 print "+ Adding additional resources +"
212 for p in config.add_resources:
213 t = os.path.join(target_res, os.path.basename(p))
217 shutil.copytree(p, t)
221 # ------------------------------------------------
223 if config.dmg is not None:
224 def runHDIUtil(verb, image_basename, **kwargs):
225 hdiutil_args = ["hdiutil", verb, image_basename + ".dmg"]
226 if kwargs.has_key("capture_stdout"):
227 del kwargs["capture_stdout"]
228 run = subprocess.check_output
231 hdiutil_args.append("-quiet")
233 hdiutil_args.append("-verbose")
234 run = subprocess.check_call
236 for key, value in kwargs.iteritems():
237 hdiutil_args.append("-" + key)
238 if not value is True:
239 hdiutil_args.append(str(value))
241 return run(hdiutil_args)
245 print "+ Creating .dmg disk image +"
247 print "+ Preparing .dmg disk image +"
250 dmg_name = config.dmg
252 spl = app_bundle_name.split(" ")
253 dmg_name = spl[0] + "".join(p.capitalize() for p in spl[1:])
257 runHDIUtil("create", dmg_name, srcfolder="dist", format="UDBZ", volname=app_bundle_name, ov=True)
258 except subprocess.CalledProcessError as e:
259 sys.exit(e.returncode)
262 print "Determining size of \"dist\"..."
264 for path, dirs, files in os.walk("dist"):
266 size += os.path.getsize(os.path.join(path, file))
267 size += int(size * 0.1)
270 print "Creating temp image for modification..."
272 runHDIUtil("create", dmg_name + ".temp", srcfolder="dist", format="UDRW", size=size, volname=app_bundle_name, ov=True)
273 except subprocess.CalledProcessError as e:
274 sys.exit(e.returncode)
277 print "Attaching temp image..."
279 output = runHDIUtil("attach", dmg_name + ".temp", readwrite=True, noverify=True, noautoopen=True, capture_stdout=True)
280 except subprocess.CalledProcessError as e:
281 sys.exit(e.returncode)
283 m = re.search("/Volumes/(.+$)", output)
284 disk_root = m.group(0)
285 disk_name = m.group(1)
288 print "+ Applying fancy settings +"
290 if fancy.has_key("background_picture"):
291 bg_path = os.path.join(disk_root, os.path.basename(fancy["background_picture"]))
293 print fancy["background_picture"], "->", bg_path
294 shutil.copy2(fancy["background_picture"], bg_path)
298 if fancy.get("applications_symlink", False):
299 os.symlink("/Applications", os.path.join(disk_root, "Applications"))
301 finder = appscript.app("Finder")
302 disk = finder.disks[disk_name]
304 window = disk.container_window
305 window.current_view.set(appscript.k.icon_view)
306 window.toolbar_visible.set(False)
307 window.statusbar_visible.set(False)
308 if fancy.has_key("window_bounds"):
309 window.bounds.set(fancy["window_bounds"])
310 view_options = window.icon_view_options
311 view_options.arrangement.set(appscript.k.not_arranged)
312 if fancy.has_key("icon_size"):
313 view_options.icon_size.set(fancy["icon_size"])
314 if bg_path is not None:
315 view_options.background_picture.set(disk.files[os.path.basename(bg_path)])
316 if fancy.has_key("items_position"):
317 for name, position in fancy["items_position"].iteritems():
318 window.items[name].position.set(position)
320 if bg_path is not None:
321 subprocess.call(["SetFile", "-a", "V", bg_path])
322 disk.update(registering_applications=False)
327 print "+ Finalizing .dmg disk image +"
330 runHDIUtil("convert", dmg_name + ".temp", format="UDBZ", o=dmg_name + ".dmg", ov=True)
331 except subprocess.CalledProcessError as e:
332 sys.exit(e.returncode)
334 os.unlink(dmg_name + ".temp.dmg")
336 # ------------------------------------------------