| # 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]) |
| |