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

« back to all changes in this revision

Viewing changes to mozilla/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