blob: efc821cf84e46c798e0afc8c01eba83ac681b71c [file] [log] [blame]
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
from optparse import OptionParser
import os
import shutil
import subprocess
import sys
import tarfile
import time
import zipfile
import mozfile
import mozinfo
try:
import pefile
has_pefile = True
except ImportError:
has_pefile = False
if mozinfo.isMac:
from plistlib import readPlist
TIMEOUT_UNINSTALL = 60
class InstallError(Exception):
"""Thrown when installation fails. Includes traceback if available."""
class InvalidBinary(Exception):
"""Thrown when the binary cannot be found after the installation."""
class InvalidSource(Exception):
"""Thrown when the specified source is not a recognized file type.
Supported types:
Linux: tar.gz, tar.bz2
Mac: dmg
Windows: zip, exe
"""
class UninstallError(Exception):
"""Thrown when uninstallation fails. Includes traceback if available."""
def get_binary(path, app_name):
"""Find the binary in the specified path, and return its path. If binary is
not found throw an InvalidBinary exception.
:param path: Path within to search for the binary
:param app_name: Application binary without file extension to look for
"""
binary = None
# On OS X we can get the real binary from the app bundle
if mozinfo.isMac:
plist = '%s/Contents/Info.plist' % path
if not os.path.isfile(plist):
raise InvalidBinary('%s/Contents/Info.plist not found' % path)
binary = os.path.join(path, 'Contents/MacOS/',
readPlist(plist)['CFBundleExecutable'])
else:
app_name = app_name.lower()
if mozinfo.isWin:
app_name = app_name + '.exe'
for root, dirs, files in os.walk(path):
for filename in files:
# os.access evaluates to False for some reason, so not using it
if filename.lower() == app_name:
binary = os.path.realpath(os.path.join(root, filename))
break
if not binary:
# The expected binary has not been found.
raise InvalidBinary('"%s" does not contain a valid binary.' % path)
return binary
def install(src, dest):
"""Install a zip, exe, tar.gz, tar.bz2 or dmg file, and return the path of
the installation folder.
:param src: Path to the install file
:param dest: Path to install to (to ensure we do not overwrite any existent
files the folder should not exist yet)
"""
src = os.path.realpath(src)
dest = os.path.realpath(dest)
if not is_installer(src):
raise InvalidSource(src + ' is not valid installer file.')
did_we_create = False
if not os.path.exists(dest):
did_we_create = True
os.makedirs(dest)
trbk = None
try:
install_dir = None
if src.lower().endswith('.dmg'):
install_dir = _install_dmg(src, dest)
elif src.lower().endswith('.exe'):
install_dir = _install_exe(src, dest)
elif zipfile.is_zipfile(src) or tarfile.is_tarfile(src):
install_dir = mozfile.extract(src, dest)[0]
return install_dir
except:
cls, exc, trbk = sys.exc_info()
if did_we_create:
try:
# try to uninstall this properly
uninstall(dest)
except:
# uninstall may fail, let's just try to clean the folder
# in this case
try:
mozfile.remove(dest)
except:
pass
if issubclass(cls, Exception):
error = InstallError('Failed to install "%s (%s)"' % (src, str(exc)))
raise InstallError, error, trbk
# any other kind of exception like KeyboardInterrupt is just re-raised.
raise cls, exc, trbk
finally:
# trbk won't get GC'ed due to circular reference
# http://docs.python.org/library/sys.html#sys.exc_info
del trbk
def is_installer(src):
"""Tests if the given file is a valid installer package.
Supported types:
Linux: tar.gz, tar.bz2
Mac: dmg
Windows: zip, exe
On Windows pefile will be used to determine if the executable is the
right type, if it is installed on the system.
:param src: Path to the install file.
"""
src = os.path.realpath(src)
if not os.path.isfile(src):
return False
if mozinfo.isLinux:
return tarfile.is_tarfile(src)
elif mozinfo.isMac:
return src.lower().endswith('.dmg')
elif mozinfo.isWin:
if zipfile.is_zipfile(src):
return True
if os.access(src, os.X_OK) and src.lower().endswith('.exe'):
if has_pefile:
# try to determine if binary is actually a gecko installer
pe_data = pefile.PE(src)
data = {}
for info in getattr(pe_data, 'FileInfo', []):
if info.Key == 'StringFileInfo':
for string in info.StringTable:
data.update(string.entries)
return 'BuildID' not in data
else:
# pefile not available, just assume a proper binary was passed in
return True
return False
def uninstall(install_folder):
"""Uninstalls the application in the specified path. If it has been
installed via an installer on Windows, use the uninstaller first.
:param install_folder: Path of the installation folder
"""
install_folder = os.path.realpath(install_folder)
assert os.path.isdir(install_folder), \
'installation folder "%s" exists.' % install_folder
# On Windows we have to use the uninstaller. If it's not available fallback
# to the directory removal code
if mozinfo.isWin:
uninstall_folder = '%s\uninstall' % install_folder
log_file = '%s\uninstall.log' % uninstall_folder
if os.path.isfile(log_file):
trbk = None
try:
cmdArgs = ['%s\uninstall\helper.exe' % install_folder, '/S']
result = subprocess.call(cmdArgs)
if result is not 0:
raise Exception('Execution of uninstaller failed.')
# The uninstaller spawns another process so the subprocess call
# returns immediately. We have to wait until the uninstall
# folder has been removed or until we run into a timeout.
end_time = time.time() + TIMEOUT_UNINSTALL
while os.path.exists(uninstall_folder):
time.sleep(1)
if time.time() > end_time:
raise Exception('Failure removing uninstall folder.')
except Exception, ex:
cls, exc, trbk = sys.exc_info()
error = UninstallError('Failed to uninstall %s (%s)' % (install_folder, str(ex)))
raise UninstallError, error, trbk
finally:
# trbk won't get GC'ed due to circular reference
# http://docs.python.org/library/sys.html#sys.exc_info
del trbk
# Ensure that we remove any trace of the installation. Even the uninstaller
# on Windows leaves files behind we have to explicitely remove.
mozfile.remove(install_folder)
def _install_dmg(src, dest):
"""Extract a dmg file into the destination folder and return the
application folder.
src -- DMG image which has to be extracted
dest -- the path to extract to
"""
try:
proc = subprocess.Popen('hdiutil attach -nobrowse -noautoopen "%s"' % src,
shell=True,
stdout=subprocess.PIPE)
for data in proc.communicate()[0].split():
if data.find('/Volumes/') != -1:
appDir = data
break
for appFile in os.listdir(appDir):
if appFile.endswith('.app'):
appName = appFile
break
mounted_path = os.path.join(appDir, appName)
dest = os.path.join(dest, appName)
# copytree() would fail if dest already exists.
if os.path.exists(dest):
raise InstallError('App bundle "%s" already exists.' % dest)
shutil.copytree(mounted_path, dest, False)
finally:
subprocess.call('hdiutil detach %s -quiet' % appDir,
shell=True)
return dest
def _install_exe(src, dest):
"""Run the MSI installer to silently install the application into the
destination folder. Return the folder path.
Arguments:
src -- MSI installer to be executed
dest -- the path to install to
"""
# The installer doesn't automatically create a sub folder. Lets guess the
# best name from the src file name
filename = os.path.basename(src)
dest = os.path.join(dest, filename.split('.')[0])
# possibly gets around UAC in vista (still need to run as administrator)
os.environ['__compat_layer'] = 'RunAsInvoker'
cmd = '"%s" /extractdir=%s' % (src, os.path.realpath(dest))
# As long as we support Python 2.4 check_call will not be available.
result = subprocess.call(cmd)
if result is not 0:
raise Exception('Execution of installer failed.')
return dest
def install_cli(argv=sys.argv[1:]):
parser = OptionParser(usage="usage: %prog [options] installer")
parser.add_option('-d', '--destination',
dest='dest',
default=os.getcwd(),
help='Directory to install application into. '
'[default: "%default"]')
parser.add_option('--app', dest='app',
default='firefox',
help='Application being installed. [default: %default]')
(options, args) = parser.parse_args(argv)
if not len(args) == 1:
parser.error('An installer file has to be specified.')
src = args[0]
# Run it
if os.path.isdir(src):
binary = get_binary(src, app_name=options.app)
else:
install_path = install(src, options.dest)
binary = get_binary(install_path, app_name=options.app)
print binary
def uninstall_cli(argv=sys.argv[1:]):
parser = OptionParser(usage="usage: %prog install_path")
(options, args) = parser.parse_args(argv)
if not len(args) == 1:
parser.error('An installation path has to be specified.')
# Run it
uninstall(argv[0])