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/.
5
"""Module to handle the installation and uninstallation of Gecko based
6
applications across platforms.
10
from optparse import OptionParser
20
from plistlib import readPlist
23
TIMEOUT_UNINSTALL = 60
26
class InstallError(Exception):
27
"""Thrown when installation fails. Includes traceback if available."""
30
class InvalidBinary(Exception):
31
"""Thrown when the binary cannot be found after the installation."""
34
class InvalidSource(Exception):
35
"""Thrown when the specified source is not a recognized file type.
38
Linux: tar.gz, tar.bz2
45
class UninstallError(Exception):
46
"""Thrown when uninstallation fails. Includes traceback if available."""
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.
54
path -- the path within to search for the binary
55
app_name -- application binary without file extension to look for
60
# On OS X we can get the real binary from the app bundle
62
plist = '%s/Contents/Info.plist' % path
63
assert os.path.isfile(plist), '"%s" has not been found.' % plist
65
binary = os.path.join(path, 'Contents/MacOS/',
66
readPlist(plist)['CFBundleExecutable'])
69
app_name = app_name.lower()
72
app_name = app_name + '.exe'
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))
82
# The expected binary has not been found. Make sure we clean the
83
# install folder to remove any traces
86
raise InvalidBinary('"%s" does not contain a valid binary.' % path)
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.
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)
101
src = os.path.realpath(src)
102
dest = os.path.realpath(dest)
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)')
108
if not os.path.exists(dest):
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)
124
cls, exc, trbk = sys.exc_info()
125
error = InstallError('Failed to install "%s"' % src)
126
raise InstallError, error, trbk
129
# trbk won't get GC'ed due to circular reference
130
# http://docs.python.org/library/sys.html#sys.exc_info
134
def is_installer(src):
135
"""Tests if the given file is a valid installer package.
138
Linux: tar.gz, tar.bz2
143
src -- the path to the install file
146
src = os.path.realpath(src)
148
if not os.path.isfile(src):
152
return tarfile.is_tarfile(src)
154
return src.lower().endswith('.dmg')
156
return src.lower().endswith('.exe') or zipfile.is_zipfile(src)
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.
164
install_folder -- the path of the installation folder
167
install_folder = os.path.realpath(install_folder)
168
assert os.path.isdir(install_folder), \
169
'installation folder "%s" exists.' % install_folder
171
# On Windows we have to use the uninstaller. If it's not available fallback
172
# to the directory removal code
174
uninstall_folder = '%s\uninstall' % install_folder
175
log_file = '%s\uninstall.log' % uninstall_folder
177
if os.path.isfile(log_file):
180
cmdArgs = ['%s\uninstall\helper.exe' % install_folder, '/S']
181
result = subprocess.call(cmdArgs)
183
raise Exception('Execution of uninstaller failed.')
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):
192
if time.time() > end_time:
193
raise Exception('Failure removing uninstall folder.')
196
cls, exc, trbk = sys.exc_info()
197
error = UninstallError('Failed to uninstall %s' % install_folder)
198
raise UninstallError, error, trbk
201
# trbk won't get GC'ed due to circular reference
202
# http://docs.python.org/library/sys.html#sys.exc_info
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)
210
def _extract(src, dest):
211
"""Extract a tar or zip file into the destination folder and return the
215
src -- archive which has to be extracted
216
dest -- the path to extract to
219
if zipfile.is_zipfile(src):
220
bundle = zipfile.ZipFile(src)
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)
229
path = os.path.dirname(filename)
230
if not os.path.isdir(path):
232
_dest = open(filename, 'wb')
233
_dest.write(bundle.read(name))
236
elif tarfile.is_tarfile(src):
237
bundle = tarfile.open(src)
238
namelist = bundle.getnames()
240
if hasattr(bundle, 'extractall'):
241
# tarfile.extractall doesn't exist in Python 2.4
242
bundle.extractall(path=dest)
244
for name in namelist:
245
bundle.extract(name, path=dest)
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]
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)
261
return top_level_files
264
def _install_dmg(src, dest):
265
"""Extract a dmg file into the destination folder and return the
269
src -- DMG image which has to be extracted
270
dest -- the path to extract to
274
proc = subprocess.Popen('hdiutil attach %s' % src,
276
stdout=subprocess.PIPE)
278
for data in proc.communicate()[0].split():
279
if data.find('/Volumes/') != -1:
283
for appFile in os.listdir(appDir):
284
if appFile.endswith('.app'):
288
mounted_path = os.path.join(appDir, appName)
290
dest = os.path.join(dest, appName)
292
# copytree() would fail if dest already exists.
293
if os.path.exists(dest):
294
raise InstallError('App bundle "%s" already exists.' % dest)
296
shutil.copytree(mounted_path, dest, False)
299
subprocess.call('hdiutil detach %s -quiet' % appDir,
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.
310
src -- MSI installer to be executed
311
dest -- the path to install to
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])
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)]
323
# As long as we support Python 2.4 check_call will not be available.
324
result = subprocess.call(cmd)
326
raise Exception('Execution of installer failed.')
331
def install_cli(argv=sys.argv[1:]):
332
parser = OptionParser(usage="usage: %prog [options] installer")
333
parser.add_option('-d', '--destination',
336
help='Directory to install application into. '
337
'[default: "%default"]')
338
parser.add_option('--app', dest='app',
340
help='Application being installed. [default: %default]')
342
(options, args) = parser.parse_args(argv)
343
if not len(args) == 1:
344
parser.error('An installer file has to be specified.')
349
if os.path.isdir(src):
350
binary = get_binary(src, app_name=options.app)
352
install_path = install(src, options.dest)
353
binary = get_binary(install_path, app_name=options.app)
358
def uninstall_cli(argv=sys.argv[1:]):
359
parser = OptionParser(usage="usage: %prog install_path")
361
(options, args) = parser.parse_args(argv)
362
if not len(args) == 1:
363
parser.error('An installation path has to be specified.')