Merge branch '0.4.x' into 0.5.x
[novacoin.git] / contrib / macdeploy / macdeployqtplus
1 #!/usr/bin/env python
2
3 #
4 # Copyright (C) 2011  Patrick "p2k" Schneider <me@p2k-network.org>
5 #
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.
10 #
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.
15 #
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/>.
18 #
19
20 import subprocess, sys, re, os, shutil, os.path
21 from time import sleep
22 from argparse import ArgumentParser
23
24 qt_conf="""[Paths]
25 translations=Resources
26 plugins=PlugIns
27 """
28
29 ap = ArgumentParser(description="""Front-end to macdeployqt with some additional functions.
30
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.
33
34 Optionally, Qt translation files (.qm) and additional resources can be added to the bundle.""")
35
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")
44
45 config = ap.parse_args()
46
47 verbose = config.verbose[0]
48
49 # ------------------------------------------------
50
51 app_bundle = config.app_bundle[0]
52
53 if not os.path.exists(app_bundle):
54     if verbose >= 1:
55         sys.stderr.write("Error: Could not find app bundle \"%s\"\n" % (app_bundle))
56     sys.exit(1)
57
58 app_bundle_name = os.path.splitext(os.path.basename(app_bundle))[0]
59
60 # ------------------------------------------------
61
62 for p in config.add_resources:
63     if verbose >= 3:
64         print "Checking for \"%s\"..." % p
65     if not os.path.exists(p):
66         if verbose >= 1:
67             sys.stderr.write("Error: Could not find additional resource file \"%s\"\n" % (p))
68         sys.exit(1)
69
70 # ------------------------------------------------
71
72 if len(config.add_qt_tr) == 0:
73     add_qt_tr = []
74 else:
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)
79         if verbose >= 3:
80             print "Checking for \"%s\"..." % p
81         if not os.path.exists(p):
82             if verbose >= 1:
83                 sys.stderr.write("Error: Could not find Qt translation file \"%s\"\n" % (lng_file))
84                 sys.exit(1)
85
86 # ------------------------------------------------
87
88 if len(config.fancy) == 1:
89     if verbose >= 3:
90         print "Fancy: Importing plistlib..."
91     try:
92         import plistlib
93     except ImportError:
94         if verbose >= 1:
95             sys.stderr.write("Error: Could not import plistlib which is required for fancy disk images.\n")
96         sys.exit(1)
97     
98     if verbose >= 3:
99         print "Fancy: Importing appscript..."
100     try:
101         import appscript
102     except ImportError:
103         if verbose >= 1:
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")
106         sys.exit(1)
107     
108     p = config.fancy[0]
109     if verbose >= 3:
110         print "Fancy: Loading \"%s\"..." % p
111     if not os.path.exists(p):
112         if verbose >= 1:
113             sys.stderr.write("Error: Could not find fancy disk image plist at \"%s\"\n" % (p))
114         sys.exit(1)
115     
116     try:
117         fancy = plistlib.readPlist(p)
118     except:
119         if verbose >= 1:
120             sys.stderr.write("Error: Could not parse fancy disk image plist at \"%s\"\n" % (p))
121         sys.exit(1)
122     
123     try:
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)
132     except:
133         if verbose >= 1:
134             sys.stderr.write("Error: Bad format of fancy disk image plist at \"%s\"\n" % (p))
135         sys.exit(1)
136     
137     if fancy.has_key("background_picture"):
138         bp = fancy["background_picture"]
139         if verbose >= 3:
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):
144                 if verbose >= 1:
145                     sys.stderr.write("Error: Could not find background picture at \"%s\" or \"%s\"\n" % (fancy["background_picture"], bp))
146                 sys.exit(1)
147             else:
148                 fancy["background_picture"] = bp
149 else:
150     fancy = None
151
152 # ------------------------------------------------
153
154 if os.path.exists("dist"):
155     if verbose >= 2:
156         print "+ Removing old dist folder +"
157     
158     shutil.rmtree("dist")
159
160 # ------------------------------------------------
161
162 target = os.path.join("dist", app_bundle)
163 target_res = os.path.join(target, "Contents", "Resources")
164
165 if verbose >= 2:
166     print "+ Copying source bundle +"
167 if verbose >= 3:
168     print app_bundle, "->", target
169
170 os.mkdir("dist")
171 shutil.copytree(app_bundle, target)
172
173 # ------------------------------------------------
174
175 macdeployqt_args = ["macdeployqt", target, "-verbose=%d" % verbose]
176 if not config.plugins:
177     macdeployqt_args.append("-no-plugins")
178 if not config.strip:
179     macdeployqt_args.append("-no-strip")
180
181 if verbose >= 2:
182     print "+ Running macdeployqt +"
183
184 ret = subprocess.call(macdeployqt_args)
185 if ret != 0:
186     sys.exit(ret)
187
188 # ------------------------------------------------
189
190 if verbose >= 2:
191     print "+ Installing qt.conf +"
192
193 f = open(os.path.join(target_res, "qt.conf"), "wb")
194 f.write(qt_conf)
195 f.close()
196
197 # ------------------------------------------------
198
199 if len(add_qt_tr) > 0 and verbose >= 2:
200     print "+ Adding Qt translations +"
201
202 for lng_file in add_qt_tr:
203     if verbose >= 3:
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))
206
207 # ------------------------------------------------
208
209 if len(config.add_resources) > 0 and verbose >= 2:
210     print "+ Adding additional resources +"
211
212 for p in config.add_resources:
213     t = os.path.join(target_res, os.path.basename(p))
214     if verbose >= 3:
215         print p, "->", t
216     if os.path.isdir(p):
217         shutil.copytree(p, t)
218     else:
219         shutil.copy2(p, t)
220
221 # ------------------------------------------------
222
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
229         else:
230             if verbose < 2:
231                 hdiutil_args.append("-quiet")
232             elif verbose >= 3:
233                 hdiutil_args.append("-verbose")
234             run = subprocess.check_call
235         
236         for key, value in kwargs.iteritems():
237             hdiutil_args.append("-" + key)
238             if not value is True:
239                 hdiutil_args.append(str(value))
240         
241         return run(hdiutil_args)
242     
243     if verbose >= 2:
244         if fancy is None:
245             print "+ Creating .dmg disk image +"
246         else:
247             print "+ Preparing .dmg disk image +"
248     
249     if config.dmg != "":
250         dmg_name = config.dmg
251     else:
252         spl = app_bundle_name.split(" ")
253         dmg_name = spl[0] + "".join(p.capitalize() for p in spl[1:])
254     
255     if fancy is None:
256         try:
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)
260     else:
261         if verbose >= 3:
262             print "Determining size of \"dist\"..."
263         size = 0
264         for path, dirs, files in os.walk("dist"):
265             for file in files:
266                 size += os.path.getsize(os.path.join(path, file))
267         size += int(size * 0.1)
268         
269         if verbose >= 3:
270             print "Creating temp image for modification..."
271         try:
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)
275         
276         if verbose >= 3:
277             print "Attaching temp image..."
278         try:
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)
282         
283         m = re.search("/Volumes/(.+$)", output)
284         disk_root = m.group(0)
285         disk_name = m.group(1)
286         
287         if verbose >= 2:
288             print "+ Applying fancy settings +"
289         
290         if fancy.has_key("background_picture"):
291             bg_path = os.path.join(disk_root, os.path.basename(fancy["background_picture"]))
292             if verbose >= 3:
293                 print fancy["background_picture"], "->", bg_path
294             shutil.copy2(fancy["background_picture"], bg_path)
295         else:
296             bg_path = None
297         
298         if fancy.get("applications_symlink", False):
299             os.symlink("/Applications", os.path.join(disk_root, "Applications"))
300         
301         finder = appscript.app("Finder")
302         disk = finder.disks[disk_name]
303         disk.open()
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)
319         disk.close()
320         if bg_path is not None:
321             subprocess.call(["SetFile", "-a", "V", bg_path])
322         disk.update(registering_applications=False)
323         sleep(2)
324         disk.eject()
325         
326         if verbose >= 2:
327             print "+ Finalizing .dmg disk image +"
328         
329         try:
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)
333         
334         os.unlink(dmg_name + ".temp.dmg")
335
336 # ------------------------------------------------
337
338 if verbose >= 2:
339     print "+ Done +"
340
341 sys.exit(0)