Make signing work on OSX
[bitmask_bundler.git] / bundler / actions.py
1 import datetime
2 import hashlib
3 import os
4 import stat
5 import sys
6 import subprocess
7 import zipfile
8 import urllib
9
10 from abc import ABCMeta, abstractmethod
11 from contextlib import contextmanager
12 from distutils import file_util, dir_util
13
14 from utils import IS_MAC, IS_WIN
15
16 if IS_MAC:
17     from sh import SetFile, hdiutil, codesign
18     from darwin_dyliber import fix_all_dylibs
19 if IS_WIN:
20     import pbs
21     from pbs import cd, glob
22     git = pbs.Command("C:\\Program Files\\Git\\bin\\git.exe")
23     python = pbs.Command("C:\\Python27\\python.exe")
24     pip = pbs.Command("C:\\Python27\\scripts\\pip.exe")
25     mkdir = pbs.Command("C:\\Program Files\\Git\\bin\\mkdir.exe")
26     make = pbs.Command("C:\\MinGW\\bin\\mingw32-make.exe")
27     cp = pbs.Command("C:\\Program Files\\Git\\bin\\cp.exe")
28     rm = pbs.Command("C:\\Program Files\\Git\\bin\\rm.exe")
29     find = pbs.Command("C:\\Program Files\\Git\\bin\\find.exe")
30     ln = pbs.Command("C:\\Program Files\\Git\\bin\\ln.exe")
31     tar = pbs.Command("C:\\Program Files\\Git\\bin\\tar.exe")
32     mv = pbs.Command("C:\\Program Files\\Git\\bin\\mv.exe")
33 else:
34     from sh import git, cd, python, mkdir, make, cp, glob, pip, rm
35     from sh import find, ln, tar, mv
36
37 from depcollector import collect_deps
38
39
40 class Action(object):
41     __metaclass__ = ABCMeta
42
43     def __init__(self, name, basedir, skip=[], do=[]):
44         self._name = name
45         self._basedir = basedir
46         self._skip = skip
47         self._do = do
48
49     @property
50     def name(self):
51         return self._name
52
53     @property
54     def skip(self):
55         return self._name in self._skip
56
57     @property
58     def do(self):
59         if len(self._do) > 0:
60             return self._name in self._do
61         return True
62
63     @abstractmethod
64     def run(self, *args, **kwargs):
65         pass
66
67
68 def skippable(func):
69     def skip_func(self, *args, **kwargs):
70         if self.skip:
71             print "Skipping...", self.name
72             return
73         if not self.do:
74             print "Skipping...", self.name
75             return
76         return func(self, *args, **kwargs)
77     return skip_func
78
79
80 def platform_dir(basedir, *args):
81     dir = os.path.join(basedir,
82                        "Bitmask",
83                        *args)
84     if IS_MAC:
85         dir = os.path.join(basedir,
86                            "Bitmask",
87                            "Bitmask.app",
88                            "Contents",
89                            "MacOS",
90                            *args)
91     return dir
92
93
94 @contextmanager
95 def push_pop(*directories):
96     cd(os.path.join(*directories))
97     yield
98     cd(os.path.join(*(("..",)*len(directories))))
99
100
101 def get_version(repos, nightly):
102     if not nightly:
103         version = "unknown"
104         with push_pop("bitmask_client"):
105             version = git("describe").strip()
106         return version
107
108     m = hashlib.sha256()
109     for repo in repos:
110         version = "unknown"
111         with push_pop(repo):
112             try:
113                 version = git("describe").strip()
114             except:
115                 pass
116         m.update(version)
117
118     return "{0}-{1}".format(str(datetime.date.today()),
119                             m.hexdigest()[:8])
120
121
122 class GitCloneAll(Action):
123     def __init__(self, basedir, skip, do):
124         Action.__init__(self, "gitclone", basedir, skip, do)
125
126     def _repo_url(self, repo_name):
127         if repo_name == "leap_assets":
128             return "git://leap.se/leap_assets"
129         return "git://github.com/leapcode/{0}".format(repo_name)
130
131     @skippable
132     def run(self, sorted_repos, nightly):
133         print "Cloning repositories..."
134         cd(self._basedir)
135         for repo in sorted_repos:
136             print "Cloning", repo
137             rm("-rf", repo)
138             git.clone(self._repo_url(repo), repo)
139             with push_pop(repo):
140                 # Thandy is a special case regarding branches, we'll just use
141                 # develop
142                 if repo in ["thandy", "leap_assets"]:
143                     continue
144                 if not nightly:
145                     git.checkout("master")
146                     git.pull("--ff-only", "origin", "master")
147                     git.fetch()
148                     git.reset("--hard", "origin/master")
149                     latest_tag = git.describe("--abbrev=0").strip()
150                     git.checkout("--quiet", latest_tag)
151                 else:
152                     git.checkout("develop")
153
154         print "Done cloning repos..."
155
156
157 class PythonSetupAll(Action):
158     def __init__(self, basedir, skip, do):
159         Action.__init__(self, "pythonsetup", basedir, skip, do)
160
161     @skippable
162     def run(self, sorted_repos):
163         cd(self._basedir)
164         for repo in sorted_repos:
165             print "Setting up", repo
166             if repo == "soledad":
167                 for subrepo in ["common", "client"]:
168                     with push_pop(repo, subrepo):
169                         pip("install", "-r", "pkg/requirements.pip")
170                         python("setup.py", "develop")
171                         sys.path.append(os.path.join(self._basedir,
172                                                      repo, subrepo, "src"))
173             elif repo in ["bitmask_launcher", "leap_assets"]:
174                 print "Skipping launcher..."
175                 continue
176             else:
177                 with push_pop(repo):
178                     if repo != "thandy":
179                         pip("install", "-r", "pkg/requirements.pip")
180                     else:
181                         # Thandy is a special kid at this point in
182                         # terms of packaging. So we install
183                         # dependencies ourselves for the time being
184                         pip("install", "pycrypto")
185                     if repo == "bitmask_client":
186                         print "Running make on the client..."
187                         make()
188                         print "Running build to get correct version..."
189                         python("setup.py", "build")
190                     python("setup.py", "develop")
191                     sys.path.append(os.path.join(self._basedir, repo, "src"))
192
193
194 def _convert_path_for_win(path):
195     npath = path
196     if IS_WIN:
197         npath = path.replace("\\", "/")
198     return npath
199
200
201 class CreateDirStructure(Action):
202     def __init__(self, basedir, skip, do):
203         Action.__init__(self, "createdirs", basedir, skip, do)
204
205     @skippable
206     def run(self):
207         print "Creating directory structure..."
208         if IS_MAC:
209             self._darwin_create_dir_structure()
210             self._create_dir_structure(os.path.join(self._basedir,
211                                                     "Bitmask.app",
212                                                     "Contents", "MacOS"))
213         else:
214             self._create_dir_structure(self._basedir)
215         print "Done"
216
217     def _create_dir_structure(self, basedir):
218         mkdirp = mkdir.bake("-p")
219         apps = os.path.join(basedir, "apps")
220         mkdirp(_convert_path_for_win(apps))
221         if IS_WIN:
222             mkdirp(_convert_path_for_win(os.path.join(apps, "eip")))
223         else:
224             mkdirp(_convert_path_for_win(os.path.join(apps, "eip", "files")))
225         mkdirp(_convert_path_for_win(os.path.join(apps, "mail")))
226         mkdirp(_convert_path_for_win(os.path.join(basedir, "lib")))
227
228     def _darwin_create_dir_structure(self):
229         mkdirp = mkdir.bake("-p")
230         app_path = os.path.join(self._basedir, "Bitmask.app")
231         mkdirp(app_path)
232         mkdirp(os.path.join(app_path, "Contents", "MacOS"))
233         mkdirp(os.path.join(app_path, "Contents", "Resources"))
234         mkdirp(os.path.join(app_path, "Contents", "PlugIns"))
235         mkdirp(os.path.join(app_path, "Contents", "StartupItems"))
236         ln("-s", "/Applications", os.path.join(self._basedir, "Applications"))
237
238
239 class CollectAllDeps(Action):
240     def __init__(self, basedir, skip, do):
241         Action.__init__(self, "collectdeps", basedir, skip, do)
242
243     def _remove_unneeded(self, lib_dir):
244         print "Removing unneeded files..."
245         files = find(lib_dir).strip().splitlines()
246         keep = ["QtCore.so",
247                 "QtGui.so",
248                 "__init__.py",
249                 "_utils.py",
250                 "PySide",
251                 ""]  # empty means the whole pyside dir
252         if IS_WIN:
253             keep = ["QtCore4.dll",
254                     "QtGui4.dll",
255                     "__init__.py",
256                     "_utils.py",
257                     "PySide",
258                     "QtGui.pyd",
259                     "QtCore.pyd",
260                     ""]  # empty means the whole pyside dir
261         for f in files:
262             if f.find("PySide") > 0:
263                 if os.path.split(f)[1] not in keep:
264                     rm("-rf", f)
265                     pass
266         print "Done"
267
268     @skippable
269     def run(self, path_file):
270         print "Collecting dependencies..."
271         app_py = os.path.join(self._basedir,
272                               "bitmask_client",
273                               "src",
274                               "leap",
275                               "bitmask",
276                               "app.py")
277         dest_lib_dir = platform_dir(self._basedir, "lib")
278         collect_deps(app_py, dest_lib_dir, path_file)
279
280         self._remove_unneeded(dest_lib_dir)
281         print "Done"
282
283
284 class CopyBinaries(Action):
285     def __init__(self, basedir, skip, do):
286         Action.__init__(self, "copybinaries", basedir, skip, do)
287
288     @skippable
289     def run(self, binaries_path):
290         print "Copying binaries..."
291         dest_lib_dir = platform_dir(self._basedir, "lib")
292
293         if IS_MAC:
294             cp(glob(os.path.join(binaries_path, "Qt*")), dest_lib_dir)
295             cp(glob(os.path.join(binaries_path, "*.dylib")), dest_lib_dir)
296             cp(glob(os.path.join(binaries_path, "Python")), dest_lib_dir)
297             resources_dir = os.path.join(self._basedir,
298                                          "Bitmask",
299                                          "Bitmask.app",
300                                          "Contents",
301                                          "Resources")
302             cp(glob(os.path.join(binaries_path, "openvpn.leap*")),
303                resources_dir)
304
305             mkdir("-p", os.path.join(resources_dir, "openvpn"))
306             cp("-r", glob(os.path.join(binaries_path, "openvpn.files", "*")),
307                os.path.join(resources_dir, "openvpn"))
308
309             cp(os.path.join(binaries_path, "cocoasudo"), resources_dir)
310
311             cp("-r", os.path.join(binaries_path, "qt_menu.nib"), resources_dir)
312             cp("-r", os.path.join(binaries_path, "tuntap-installer.app"),
313                resources_dir)
314             cp(os.path.join(binaries_path, "Bitmask"),
315                platform_dir(self._basedir))
316         elif IS_WIN:
317             root = _convert_path_for_win(
318                 os.path.join(self._basedir, "Bitmask"))
319             for i in glob(os.path.join(binaries_path, "*.dll")):
320                 cp(_convert_path_for_win(i),
321                    root)
322             import win32com
323             win32comext_path = os.path.split(win32com.__file__)[0] + "ext"
324             shell_path = os.path.join(win32comext_path, "shell")
325             cp("-r",
326                _convert_path_for_win(shell_path),
327                _convert_path_for_win(os.path.join(dest_lib_dir, "win32com")))
328             cp(_convert_path_for_win(
329                 os.path.join(binaries_path, "bitmask.exe")),
330                root)
331             cp(_convert_path_for_win(
332                 os.path.join(binaries_path, "Microsoft.VC90.CRT.manifest")),
333                root)
334             cp(_convert_path_for_win(
335                 os.path.join(binaries_path, "openvpn_leap.exe")),
336                _convert_path_for_win(
337                    os.path.join(root, "apps", "eip")))
338             cp(_convert_path_for_win(
339                 os.path.join(binaries_path, "openvpn_leap.exe.manifest")),
340                _convert_path_for_win(
341                    os.path.join(root, "apps", "eip")))
342             cp("-r",
343                _convert_path_for_win(
344                    os.path.join(binaries_path, "tap_driver")),
345                _convert_path_for_win(
346                    os.path.join(root, "apps", "eip")))
347         else:
348             cp(glob(os.path.join(binaries_path, "*.so*")), dest_lib_dir)
349
350             eip_dir = platform_dir(self._basedir, "apps", "eip")
351             #cp(os.path.join(binaries_path, "openvpn"), eip_dir)
352
353             cp("-r", glob(os.path.join(binaries_path, "openvpn.files", "*")),
354                os.path.join(eip_dir, "files"))
355             cp(os.path.join(binaries_path, "bitmask"),
356                platform_dir(self._basedir))
357
358         mail_dir = platform_dir(self._basedir, "apps", "mail")
359         cp(_convert_path_for_win(os.path.join(binaries_path, "gpg")),
360            _convert_path_for_win(mail_dir))
361         print "Done"
362
363
364 class PLister(Action):
365     plist = """<?xml version="1.0" encoding="UTF-8"?>
366 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
367 <plist version="1.0">
368 <dict>
369         <key>CFBundleDisplayName</key>
370         <string>Bitmask</string>
371         <key>CFBundleExecutable</key>
372         <string>MacOS/bitmask-launcher</string>
373         <key>CFBundleIconFile</key>
374         <string>bitmask.icns</string>
375         <key>CFBundleInfoDictionaryVersion</key>
376         <string>6.0</string>
377         <key>CFBundleName</key>
378         <string>Bitmask</string>
379         <key>CFBundlePackageType</key>
380         <string>APPL</string>
381         <key>CFBundleShortVersionString</key>
382         <string>1</string>
383         <key>LSBackgroundOnly</key>
384         <false/>
385         <key>CFBundleIdentifier</key>
386         <string>se.leap.bitmask</string>
387 </dict>
388 </plist>""".split("\n")
389
390     qtconf = """[Paths]
391 Plugins = PlugIns"""
392
393     def __init__(self, basedir, skip, do):
394         Action.__init__(self, "plister", basedir, skip, do)
395
396     @skippable
397     def run(self):
398         print "Generating Info.plist file..."
399         file_util.write_file(os.path.join(self._basedir,
400                                           "Bitmask",
401                                           "Bitmask.app",
402                                           "Contents",
403                                           "Info.plist"),
404                              self.plist)
405         print "Generating qt.conf file..."
406         file_util.write_file(os.path.join(self._basedir,
407                                           "Bitmask",
408                                           "Bitmask.app",
409                                           "Contents",
410                                           "Resources",
411                                           "qt.conf"),
412                              self.qtconf)
413         print "Done"
414
415
416 class SeededConfig(Action):
417     def __init__(self, basedir, skip, do):
418         Action.__init__(self, "seededconfig", basedir, skip, do)
419
420     @skippable
421     def run(self, seeded_config):
422         print "Copying seeded config..."
423         dir_util.copy_tree(seeded_config,
424                            platform_dir(self._basedir, "config"))
425         print "Done"
426
427
428 class DarwinLauncher(Action):
429     launcher = """#!/bin/bash
430 #
431 # Launcher for the LEAP Client under OSX
432 #
433 DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"
434 export DYLD_LIBRARY_PATH=$DIR/lib
435 export PATH=$DIR/../Resources/:$PATH
436 # ---------------------------
437 # DEBUG Info -- enable this if you
438 # are having problems with dynamic libraries loading
439
440 cd "${DIR}" && ./Bitmask $1 $2 $3 $4 $5""".split("\n")
441
442     def __init__(self, basedir, skip, do):
443         Action.__init__(self, "darwinlauncher", basedir, skip, do)
444
445     @skippable
446     def run(self):
447         print "Generating launcher script for OSX..."
448         launcher_path = os.path.join(self._basedir,
449                                      "Bitmask",
450                                      "Bitmask.app",
451                                      "Contents",
452                                      "MacOS",
453                                      "bitmask-launcher")
454         file_util.write_file(launcher_path, self.launcher)
455         os.chmod(launcher_path, stat.S_IRGRP | stat.S_IROTH | stat.S_IRUSR
456                  | stat.S_IWGRP | stat.S_IWOTH | stat.S_IWUSR
457                  | stat.S_IXGRP | stat.S_IXOTH | stat.S_IXUSR)
458         print "Done"
459
460
461 class CopyAssets(Action):
462     def __init__(self, basedir, skip, do):
463         Action.__init__(self, "copyassets", basedir, skip, do)
464
465     @skippable
466     def run(self):
467         print "Copying assets..."
468         resources_dir = os.path.join(self._basedir,
469                                      "Bitmask",
470                                      "Bitmask.app",
471                                      "Contents",
472                                      "Resources")
473         cp(os.path.join(self._basedir, "leap_assets", "mac", "bitmask.icns"),
474            resources_dir)
475         cp(os.path.join(self._basedir, "leap_assets", "mac", "bitmask.tiff"),
476            resources_dir)
477         print "Done"
478
479
480 class CopyMisc(Action):
481     def __init__(self, basedir, skip, do):
482         Action.__init__(self, "copymisc", basedir, skip, do)
483
484     @skippable
485     def run(self):
486         print "Downloading thunderbird extension..."
487         ext_path = platform_dir(self._basedir, "apps",
488                                 "bitmask-thunderbird-latest.xpi")
489         urllib.urlretrieve(
490             "https://downloads.leap.se/thunderbird_extension/"
491             "bitmask-thunderbird-latest.xpi",
492             ext_path)
493         print "Done"
494         print "Copying misc files..."
495         apps_dir = _convert_path_for_win(platform_dir(self._basedir, "apps"))
496         cp(_convert_path_for_win(
497             os.path.join(self._basedir, "bitmask_launcher", "src",
498                          "launcher.py")),
499            apps_dir)
500         cp("-r",
501            _convert_path_for_win(os.path.join(self._basedir, "thandy", "lib",
502                                               "thandy")),
503            apps_dir)
504         cp("-r",
505            _convert_path_for_win(os.path.join(self._basedir, "bitmask_client",
506                                               "src", "leap")),
507            apps_dir)
508         lib_dir = _convert_path_for_win(platform_dir(self._basedir, "lib"))
509         cp(_convert_path_for_win(
510             os.path.join(self._basedir,
511                          "leap_pycommon",
512                          "src", "leap", "common", "cacert.pem")),
513            _convert_path_for_win(os.path.join(lib_dir, "leap", "common")))
514         cp(_convert_path_for_win(glob(os.path.join(self._basedir,
515                                                    "bitmask_client", "build",
516                                                    "lib*", "leap", "bitmask",
517                                                    "_version.py"))[0]),
518            os.path.join(apps_dir, "leap", "bitmask"))
519
520         cp(_convert_path_for_win(
521             os.path.join(self._basedir,
522                          "bitmask_client", "relnotes.txt")),
523            _convert_path_for_win(os.path.join(self._basedir, "Bitmask")))
524         print "Done"
525
526
527 class FixDylibs(Action):
528     def __init__(self, basedir, skip, do):
529         Action.__init__(self, "fixdylibs", basedir, skip, do)
530
531     @skippable
532     def run(self):
533         fix_all_dylibs(platform_dir(self._basedir))
534
535
536 class DmgIt(Action):
537     def __init__(self, basedir, skip, do):
538         Action.__init__(self, "dmgit", basedir, skip, do)
539
540     @skippable
541     def run(self, repos, nightly):
542         print "Dmg'ing it..."
543         cd(self._basedir)
544         version = get_version(repos, nightly)
545         dmg_dir = os.path.join(self._basedir, "dmg")
546         template_dir = os.path.join(self._basedir, "Bitmask")
547         mkdir("-p", dmg_dir)
548         cp("-R", os.path.join(template_dir, "Applications"), dmg_dir)
549         cp("-R", os.path.join(template_dir, "relnotes.txt"), dmg_dir)
550         cp("-R", os.path.join(template_dir, "Bitmask.app"), dmg_dir)
551         cp(os.path.join(self._basedir,
552                         "leap_assets",
553                         "mac", "bitmask.icns"),
554            os.path.join(dmg_dir, ".VolumeIcon.icns"))
555         SetFile("-c", "icnC", os.path.join(dmg_dir, ".VolumeIcon.icns"))
556
557         vol_name = "Bitmask"
558         dmg_name = "Bitmask-OSX-{0}.dmg".format(version)
559         raw_dmg_path = os.path.join(self._basedir, "raw-{0}".format(dmg_name))
560         dmg_path = os.path.join(self._basedir, dmg_name)
561
562         hdiutil("create", "-srcfolder", dmg_dir, "-volname", vol_name,
563                 "-fsargs", "-c c=64,a=16,e=16", "-fs", "HFS+",
564                 "-format", "UDRW", "-ov", "-size", "500000k",
565                 raw_dmg_path)
566         rm("-rf", dmg_dir)
567         mkdir(dmg_dir)
568         hdiutil("attach", raw_dmg_path, "-mountpoint", dmg_dir)
569         SetFile("-a", "C", dmg_dir)
570         hdiutil("detach", dmg_dir)
571
572         rm("-rf", dmg_dir)
573         hdiutil("convert", raw_dmg_path, "-format", "UDZO",
574                 "-imagekey", "zlib-level=9", "-o",
575                 dmg_path)
576         rm("-f", raw_dmg_path)
577         print "Done"
578
579
580 class TarballIt(Action):
581     def __init__(self, basedir, skip, do):
582         Action.__init__(self, "tarballit", basedir, skip, do)
583
584     @skippable
585     def run(self, repos, nightly):
586         print "Tarballing it..."
587         cd(self._basedir)
588         version = get_version(repos, nightly)
589         import platform
590         bits = platform.architecture()[0][:2]
591         bundle_name = "Bitmask-linux%s-%s" % (bits, version)
592         mv("Bitmask", bundle_name)
593         tar("cjf", bundle_name+".tar.bz2", bundle_name)
594         print "Done"
595
596
597 class PycRemover(Action):
598     def __init__(self, basedir, skip, do):
599         Action.__init__(self, "removepyc", basedir, skip, do)
600
601     @skippable
602     def run(self):
603         print "Removing .pyc files..."
604         files = find(self._basedir, "-name", "*.pyc").strip().splitlines()
605         for f in files:
606             rm(f)
607         print "Done"
608
609
610 class MtEmAll(Action):
611     def __init__(self, basedir, skip, do):
612         Action.__init__(self, "mtemall", basedir, skip, do)
613
614     @skippable
615     def run(self):
616         print "Mt'ing all the files..."
617         cd(os.path.join(self._basedir, "Bitmask"))
618         subprocess.check_call(
619             ["C:\\Program Files\\Windows Kits\\8.0\\bin\\x86\\mt.exe",
620              "-nologo", "-manifest", "Microsoft.VC90.CRT.manifest",
621              "-outputresource:bitmask.exe;#1"])
622         cd(os.path.join("apps", "eip"))
623         subprocess.check_call(
624             ["C:\\Program Files\\Windows Kits\\8.0\\bin\\x86\\mt.exe",
625              "-nologo", "-manifest", "openvpn_leap.exe.manifest",
626              "-outputresource:openvpn_leap.exe;#1"])
627         print "Done"
628
629
630 class ZipIt(Action):
631     def __init__(self, basedir, skip, do):
632         Action.__init__(self, "zipit", basedir, skip, do)
633
634     def _zipdir(self, path, zf):
635         for root, dirs, files in os.walk(path):
636             for f in files:
637                 zf.write(os.path.join(root, f))
638
639     @skippable
640     def run(self, repos, nightly):
641         print "Ziping it..."
642         cd(self._basedir)
643         version = get_version(repos, nightly)
644         name = "Bitmask-win32-{0}".format(version)
645         mv(_convert_path_for_win(os.path.join(self._basedir, "Bitmask")),
646            _convert_path_for_win(os.path.join(self._basedir, name)))
647         zf = zipfile.ZipFile("{0}.zip".format(name), "w", zipfile.ZIP_DEFLATED)
648         self._zipdir(name, zf)
649         zf.close()
650         print "Done"
651
652
653 class SignIt(Action):
654     def __init__(self, basedir, skip, do):
655         Action.__init__(self, "signit", basedir, skip, do)
656
657     @skippable
658     def run(self, identity):
659         print "Signing tuntap kext..."
660         kext = os.path.join(self._basedir,
661                             "Bitmask",
662                             "Bitmask.app",
663                             "Contents",
664                             "Resources",
665                             "tuntap-installer.app",
666                             "Contents",
667                             "Extensions",
668                             "tun.kext")
669         codesign("-s", identity, "--deep", kext)
670         print "Done"
671         print "Signing tuntap installer..."
672         tuntap_app = os.path.join(self._basedir,
673                                   "Bitmask",
674                                   "Bitmask.app",
675                                   "Contents",
676                                   "Resources",
677                                   "tuntap-installer.app")
678         codesign("-s", identity, "--deep", tuntap_app)
679         print "Done"
680         print "Signing main structure, this will take a while..."
681         main_app = os.path.join(self._basedir,
682                                 "Bitmask",
683                                 "Bitmask.app")
684         print codesign("-s", identity, "--force", "--deep", "--verbose", main_app)
685         print "Done"