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