~ubuntu-branches/ubuntu/quantal/enigmail/quantal-security

« back to all changes in this revision

Viewing changes to testing/mozbase/mozinstall/mozinstall/mozinstall.py

  • Committer: Package Import Robot
  • Author(s): Chris Coulson
  • Date: 2013-09-13 16:02:15 UTC
  • mfrom: (0.12.16)
  • Revision ID: package-import@ubuntu.com-20130913160215-u3g8nmwa0pdwagwc
Tags: 2:1.5.2-0ubuntu0.12.10.1
* New upstream release v1.5.2 for Thunderbird 24

* Build enigmail using a stripped down Thunderbird 17 build system, as it's
  now quite difficult to build the way we were doing previously, with the
  latest Firefox build system
* Add debian/patches/no_libxpcom.patch - Don't link against libxpcom, as it
  doesn't exist anymore (but exists in the build system)
* Add debian/patches/use_sdk.patch - Use the SDK version of xpt.py and
  friends
* Drop debian/patches/ipc-pipe_rename.diff (not needed anymore)
* Drop debian/patches/makefile_depth.diff (not needed anymore)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# This Source Code Form is subject to the terms of the Mozilla Public
2
 
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
 
# You can obtain one at http://mozilla.org/MPL/2.0/.
4
 
 
5
 
"""Module to handle the installation and uninstallation of Gecko based
6
 
applications across platforms.
7
 
 
8
 
"""
9
 
import mozinfo
10
 
from optparse import OptionParser
11
 
import os
12
 
import shutil
13
 
import subprocess
14
 
import sys
15
 
import tarfile
16
 
import time
17
 
import zipfile
18
 
 
19
 
if mozinfo.isMac:
20
 
    from plistlib import readPlist
21
 
 
22
 
 
23
 
TIMEOUT_UNINSTALL = 60
24
 
 
25
 
 
26
 
class InstallError(Exception):
27
 
    """Thrown when installation fails. Includes traceback if available."""
28
 
 
29
 
 
30
 
class InvalidBinary(Exception):
31
 
    """Thrown when the binary cannot be found after the installation."""
32
 
 
33
 
 
34
 
class InvalidSource(Exception):
35
 
    """Thrown when the specified source is not a recognized file type.
36
 
 
37
 
    Supported types:
38
 
    Linux:   tar.gz, tar.bz2
39
 
    Mac:     dmg
40
 
    Windows: zip, exe
41
 
 
42
 
    """
43
 
 
44
 
 
45
 
class UninstallError(Exception):
46
 
    """Thrown when uninstallation fails. Includes traceback if available."""
47
 
 
48
 
 
49
 
def get_binary(path, app_name):
50
 
    """Find the binary in the specified path, and return its path. If binary is
51
 
    not found throw an InvalidBinary exception.
52
 
 
53
 
    Arguments:
54
 
    path -- the path within to search for the binary
55
 
    app_name -- application binary without file extension to look for
56
 
 
57
 
    """
58
 
    binary = None
59
 
 
60
 
    # On OS X we can get the real binary from the app bundle
61
 
    if mozinfo.isMac:
62
 
        plist = '%s/Contents/Info.plist' % path
63
 
        assert os.path.isfile(plist), '"%s" has not been found.' % plist
64
 
 
65
 
        binary = os.path.join(path, 'Contents/MacOS/',
66
 
                              readPlist(plist)['CFBundleExecutable'])
67
 
 
68
 
    else:
69
 
        app_name = app_name.lower()
70
 
 
71
 
        if mozinfo.isWin:
72
 
            app_name = app_name + '.exe'
73
 
 
74
 
        for root, dirs, files in os.walk(path):
75
 
            for filename in files:
76
 
                # os.access evaluates to False for some reason, so not using it
77
 
                if filename.lower() == app_name:
78
 
                    binary = os.path.realpath(os.path.join(root, filename))
79
 
                    break
80
 
 
81
 
    if not binary:
82
 
        # The expected binary has not been found. Make sure we clean the
83
 
        # install folder to remove any traces
84
 
        shutil.rmtree(path)
85
 
 
86
 
        raise InvalidBinary('"%s" does not contain a valid binary.' % path)
87
 
 
88
 
    return binary
89
 
 
90
 
 
91
 
def install(src, dest):
92
 
    """Install a zip, exe, tar.gz, tar.bz2 or dmg file, and return the path of
93
 
    the installation folder.
94
 
 
95
 
    Arguments:
96
 
    src  -- the path to the install file
97
 
    dest -- the path to install to (to ensure we do not overwrite any existent
98
 
                                    files the folder should not exist yet)
99
 
 
100
 
    """
101
 
    src = os.path.realpath(src)
102
 
    dest = os.path.realpath(dest)
103
 
 
104
 
    if not is_installer(src):
105
 
        raise InvalidSource(src + ' is not a recognized file type ' +
106
 
                                  '(zip, exe, tar.gz, tar.bz2 or dmg)')
107
 
 
108
 
    if not os.path.exists(dest):
109
 
        os.makedirs(dest)
110
 
 
111
 
    trbk = None
112
 
    try:
113
 
        install_dir = None
114
 
        if zipfile.is_zipfile(src) or tarfile.is_tarfile(src):
115
 
            install_dir = _extract(src, dest)[0]
116
 
        elif src.lower().endswith('.dmg'):
117
 
            install_dir = _install_dmg(src, dest)
118
 
        elif src.lower().endswith('.exe'):
119
 
            install_dir = _install_exe(src, dest)
120
 
 
121
 
        return install_dir
122
 
 
123
 
    except Exception, e:
124
 
        cls, exc, trbk = sys.exc_info()
125
 
        error = InstallError('Failed to install "%s"' % src)
126
 
        raise InstallError, error, trbk
127
 
 
128
 
    finally:
129
 
        # trbk won't get GC'ed due to circular reference
130
 
        # http://docs.python.org/library/sys.html#sys.exc_info
131
 
        del trbk
132
 
 
133
 
 
134
 
def is_installer(src):
135
 
    """Tests if the given file is a valid installer package.
136
 
 
137
 
    Supported types:
138
 
    Linux:   tar.gz, tar.bz2
139
 
    Mac:     dmg
140
 
    Windows: zip, exe
141
 
 
142
 
    Arguments:
143
 
    src -- the path to the install file
144
 
 
145
 
    """
146
 
    src = os.path.realpath(src)
147
 
 
148
 
    if not os.path.isfile(src):
149
 
        return False
150
 
 
151
 
    if mozinfo.isLinux:
152
 
        return tarfile.is_tarfile(src)
153
 
    elif mozinfo.isMac:
154
 
        return src.lower().endswith('.dmg')
155
 
    elif mozinfo.isWin:
156
 
        return src.lower().endswith('.exe') or zipfile.is_zipfile(src)
157
 
 
158
 
 
159
 
def uninstall(install_folder):
160
 
    """Uninstalls the application in the specified path. If it has been
161
 
    installed via an installer on Windows, use the uninstaller first.
162
 
 
163
 
    Arguments:
164
 
    install_folder -- the path of the installation folder
165
 
 
166
 
    """
167
 
    install_folder = os.path.realpath(install_folder)
168
 
    assert os.path.isdir(install_folder), \
169
 
        'installation folder "%s" exists.' % install_folder
170
 
 
171
 
    # On Windows we have to use the uninstaller. If it's not available fallback
172
 
    # to the directory removal code
173
 
    if mozinfo.isWin:
174
 
        uninstall_folder = '%s\uninstall' % install_folder
175
 
        log_file = '%s\uninstall.log' % uninstall_folder
176
 
 
177
 
        if os.path.isfile(log_file):
178
 
            trbk = None
179
 
            try:
180
 
                cmdArgs = ['%s\uninstall\helper.exe' % install_folder, '/S']
181
 
                result = subprocess.call(cmdArgs)
182
 
                if not result is 0:
183
 
                    raise Exception('Execution of uninstaller failed.')
184
 
 
185
 
                # The uninstaller spawns another process so the subprocess call
186
 
                # returns immediately. We have to wait until the uninstall
187
 
                # folder has been removed or until we run into a timeout.
188
 
                end_time = time.time() + TIMEOUT_UNINSTALL
189
 
                while os.path.exists(uninstall_folder):
190
 
                    time.sleep(1)
191
 
 
192
 
                    if time.time() > end_time:
193
 
                        raise Exception('Failure removing uninstall folder.')
194
 
 
195
 
            except Exception, e:
196
 
                cls, exc, trbk = sys.exc_info()
197
 
                error = UninstallError('Failed to uninstall %s' % install_folder)
198
 
                raise UninstallError, error, trbk
199
 
 
200
 
            finally:
201
 
                # trbk won't get GC'ed due to circular reference
202
 
                # http://docs.python.org/library/sys.html#sys.exc_info
203
 
                del trbk
204
 
 
205
 
    # Ensure that we remove any trace of the installation. Even the uninstaller
206
 
    # on Windows leaves files behind we have to explicitely remove.
207
 
    shutil.rmtree(install_folder)
208
 
 
209
 
 
210
 
def _extract(src, dest):
211
 
    """Extract a tar or zip file into the destination folder and return the
212
 
    application folder.
213
 
 
214
 
    Arguments:
215
 
    src -- archive which has to be extracted
216
 
    dest -- the path to extract to
217
 
 
218
 
    """
219
 
    if zipfile.is_zipfile(src):
220
 
        bundle = zipfile.ZipFile(src)
221
 
 
222
 
        # FIXME: replace with zip.extractall() when we require python 2.6
223
 
        namelist = bundle.namelist()
224
 
        for name in bundle.namelist():
225
 
            filename = os.path.realpath(os.path.join(dest, name))
226
 
            if name.endswith('/'):
227
 
                os.makedirs(filename)
228
 
            else:
229
 
                path = os.path.dirname(filename)
230
 
                if not os.path.isdir(path):
231
 
                    os.makedirs(path)
232
 
                _dest = open(filename, 'wb')
233
 
                _dest.write(bundle.read(name))
234
 
                _dest.close()
235
 
 
236
 
    elif tarfile.is_tarfile(src):
237
 
        bundle = tarfile.open(src)
238
 
        namelist = bundle.getnames()
239
 
 
240
 
        if hasattr(bundle, 'extractall'):
241
 
            # tarfile.extractall doesn't exist in Python 2.4
242
 
            bundle.extractall(path=dest)
243
 
        else:
244
 
            for name in namelist:
245
 
                bundle.extract(name, path=dest)
246
 
    else:
247
 
        return
248
 
 
249
 
    bundle.close()
250
 
 
251
 
    # namelist returns paths with forward slashes even in windows
252
 
    top_level_files = [os.path.join(dest, name) for name in namelist
253
 
                             if len(name.rstrip('/').split('/')) == 1]
254
 
 
255
 
    # namelist doesn't include folders, append these to the list
256
 
    for name in namelist:
257
 
        root = os.path.join(dest, name[:name.find('/')])
258
 
        if root not in top_level_files:
259
 
            top_level_files.append(root)
260
 
 
261
 
    return top_level_files
262
 
 
263
 
 
264
 
def _install_dmg(src, dest):
265
 
    """Extract a dmg file into the destination folder and return the
266
 
    application folder.
267
 
 
268
 
    Arguments:
269
 
    src -- DMG image which has to be extracted
270
 
    dest -- the path to extract to
271
 
 
272
 
    """
273
 
    try:
274
 
        proc = subprocess.Popen('hdiutil attach %s' % src,
275
 
                                shell=True,
276
 
                                stdout=subprocess.PIPE)
277
 
 
278
 
        for data in proc.communicate()[0].split():
279
 
            if data.find('/Volumes/') != -1:
280
 
                appDir = data
281
 
                break
282
 
 
283
 
        for appFile in os.listdir(appDir):
284
 
            if appFile.endswith('.app'):
285
 
                appName = appFile
286
 
                break
287
 
 
288
 
        mounted_path = os.path.join(appDir, appName)
289
 
 
290
 
        dest = os.path.join(dest, appName)
291
 
 
292
 
        # copytree() would fail if dest already exists.
293
 
        if os.path.exists(dest):
294
 
            raise InstallError('App bundle "%s" already exists.' % dest)
295
 
 
296
 
        shutil.copytree(mounted_path, dest, False)
297
 
 
298
 
    finally:
299
 
        subprocess.call('hdiutil detach %s -quiet' % appDir,
300
 
                        shell=True)
301
 
 
302
 
    return dest
303
 
 
304
 
 
305
 
def _install_exe(src, dest):
306
 
    """Run the MSI installer to silently install the application into the
307
 
    destination folder. Return the folder path.
308
 
 
309
 
    Arguments:
310
 
    src -- MSI installer to be executed
311
 
    dest -- the path to install to
312
 
 
313
 
    """
314
 
    # The installer doesn't automatically create a sub folder. Lets guess the
315
 
    # best name from the src file name
316
 
    filename = os.path.basename(src)
317
 
    dest = os.path.join(dest, filename.split('.')[0])
318
 
 
319
 
    # possibly gets around UAC in vista (still need to run as administrator)
320
 
    os.environ['__compat_layer'] = 'RunAsInvoker'
321
 
    cmd = [src, '/S', '/D=%s' % os.path.realpath(dest)]
322
 
 
323
 
    # As long as we support Python 2.4 check_call will not be available.
324
 
    result = subprocess.call(cmd)
325
 
    if not result is 0:
326
 
        raise Exception('Execution of installer failed.')
327
 
 
328
 
    return dest
329
 
 
330
 
 
331
 
def install_cli(argv=sys.argv[1:]):
332
 
    parser = OptionParser(usage="usage: %prog [options] installer")
333
 
    parser.add_option('-d', '--destination',
334
 
                      dest='dest',
335
 
                      default=os.getcwd(),
336
 
                      help='Directory to install application into. '
337
 
                           '[default: "%default"]')
338
 
    parser.add_option('--app', dest='app',
339
 
                      default='firefox',
340
 
                      help='Application being installed. [default: %default]')
341
 
 
342
 
    (options, args) = parser.parse_args(argv)
343
 
    if not len(args) == 1:
344
 
        parser.error('An installer file has to be specified.')
345
 
 
346
 
    src = args[0]
347
 
 
348
 
    # Run it
349
 
    if os.path.isdir(src):
350
 
        binary = get_binary(src, app_name=options.app)
351
 
    else:
352
 
        install_path = install(src, options.dest)
353
 
        binary = get_binary(install_path, app_name=options.app)
354
 
 
355
 
    print binary
356
 
 
357
 
 
358
 
def uninstall_cli(argv=sys.argv[1:]):
359
 
    parser = OptionParser(usage="usage: %prog install_path")
360
 
 
361
 
    (options, args) = parser.parse_args(argv)
362
 
    if not len(args) == 1:
363
 
        parser.error('An installation path has to be specified.')
364
 
 
365
 
    # Run it
366
 
    uninstall(argv[0])
367