diff options
Diffstat (limited to 'testing/mozbase/mozinstall')
-rw-r--r-- | testing/mozbase/mozinstall/mozinstall/__init__.py | 6 | ||||
-rwxr-xr-x | testing/mozbase/mozinstall/mozinstall/mozinstall.py | 342 | ||||
-rw-r--r-- | testing/mozbase/mozinstall/setup.py | 53 | ||||
-rw-r--r-- | testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg | bin | 0 -> 13441 bytes | |||
-rw-r--r-- | testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2 | bin | 0 -> 2882 bytes | |||
-rw-r--r-- | testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip | bin | 0 -> 8707 bytes | |||
-rw-r--r-- | testing/mozbase/mozinstall/tests/manifest.ini | 1 | ||||
-rw-r--r-- | testing/mozbase/mozinstall/tests/test.py | 169 |
8 files changed, 571 insertions, 0 deletions
diff --git a/testing/mozbase/mozinstall/mozinstall/__init__.py b/testing/mozbase/mozinstall/mozinstall/__init__.py new file mode 100644 index 000000000..5f96b7fac --- /dev/null +++ b/testing/mozbase/mozinstall/mozinstall/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa +# 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 mozinstall import * diff --git a/testing/mozbase/mozinstall/mozinstall/mozinstall.py b/testing/mozbase/mozinstall/mozinstall/mozinstall.py new file mode 100755 index 000000000..b4c6f95f7 --- /dev/null +++ b/testing/mozbase/mozinstall/mozinstall/mozinstall.py @@ -0,0 +1,342 @@ +# 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]) diff --git a/testing/mozbase/mozinstall/setup.py b/testing/mozbase/mozinstall/setup.py new file mode 100644 index 000000000..7759f0728 --- /dev/null +++ b/testing/mozbase/mozinstall/setup.py @@ -0,0 +1,53 @@ +# 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/. + +import os +from setuptools import setup + +try: + here = os.path.dirname(os.path.abspath(__file__)) + description = file(os.path.join(here, 'README.md')).read() +except IOError: + description = None + +PACKAGE_VERSION = '1.12' + +deps = ['mozinfo >= 0.7', + 'mozfile >= 1.0', + ] + +setup(name='mozInstall', + version=PACKAGE_VERSION, + description="package for installing and uninstalling Mozilla applications", + long_description="see http://mozbase.readthedocs.org/", + # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=['Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + keywords='mozilla', + author='Mozilla Automation and Tools team', + author_email='tools@lists.mozilla.org', + url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + license='MPL 2.0', + packages=['mozinstall'], + include_package_data=True, + zip_safe=False, + install_requires=deps, + tests_require=['mozprocess >= 0.15', ], + # we have to generate two more executables for those systems that cannot run as Administrator + # and the filename containing "install" triggers the UAC + entry_points=""" + # -*- Entry points: -*- + [console_scripts] + mozinstall = mozinstall:install_cli + mozuninstall = mozinstall:uninstall_cli + moz_add_to_system = mozinstall:install_cli + moz_remove_from_system = mozinstall:uninstall_cli + """, + ) diff --git a/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg Binary files differnew file mode 100644 index 000000000..f7f36f631 --- /dev/null +++ b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg diff --git a/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2 b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2 Binary files differnew file mode 100644 index 000000000..cb046a0e7 --- /dev/null +++ b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2 diff --git a/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip Binary files differnew file mode 100644 index 000000000..7c3f61a5e --- /dev/null +++ b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip diff --git a/testing/mozbase/mozinstall/tests/manifest.ini b/testing/mozbase/mozinstall/tests/manifest.ini new file mode 100644 index 000000000..528fdea7b --- /dev/null +++ b/testing/mozbase/mozinstall/tests/manifest.ini @@ -0,0 +1 @@ +[test.py] diff --git a/testing/mozbase/mozinstall/tests/test.py b/testing/mozbase/mozinstall/tests/test.py new file mode 100644 index 000000000..b4c53bb42 --- /dev/null +++ b/testing/mozbase/mozinstall/tests/test.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python + +# 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/. + +import mozinfo +import mozinstall +import mozfile +import os +import tempfile +import unittest + +# Store file location at load time +here = os.path.dirname(os.path.abspath(__file__)) + + +class TestMozInstall(unittest.TestCase): + + @classmethod + def setUpClass(cls): + """ Setting up stub installers """ + cls.dmg = os.path.join(here, 'Installer-Stubs', 'firefox.dmg') + # XXX: We have removed firefox.exe since it is not valid for mozinstall 1.12 and higher + # Bug 1157352 - We should grab a firefox.exe from the build process or download it + cls.exe = os.path.join(here, 'Installer-Stubs', 'firefox.exe') + cls.zipfile = os.path.join(here, 'Installer-Stubs', 'firefox.zip') + cls.bz2 = os.path.join(here, 'Installer-Stubs', 'firefox.tar.bz2') + + def setUp(self): + self.tempdir = tempfile.mkdtemp() + + def tearDown(self): + mozfile.rmtree(self.tempdir) + + @unittest.skipIf(mozinfo.isWin, "Bug 1157352 - We need a new firefox.exe " + "for mozinstall 1.12 and higher.") + def test_get_binary(self): + """ Test mozinstall's get_binary method """ + + if mozinfo.isLinux: + installdir = mozinstall.install(self.bz2, self.tempdir) + binary = os.path.join(installdir, 'firefox') + self.assertEqual(binary, mozinstall.get_binary(installdir, 'firefox')) + + elif mozinfo.isWin: + installdir_exe = mozinstall.install(self.exe, + os.path.join(self.tempdir, 'exe')) + binary_exe = os.path.join(installdir_exe, 'core', 'firefox.exe') + self.assertEqual(binary_exe, mozinstall.get_binary(installdir_exe, + 'firefox')) + + installdir_zip = mozinstall.install(self.zipfile, + os.path.join(self.tempdir, 'zip')) + binary_zip = os.path.join(installdir_zip, 'firefox.exe') + self.assertEqual(binary_zip, mozinstall.get_binary(installdir_zip, + 'firefox')) + + elif mozinfo.isMac: + installdir = mozinstall.install(self.dmg, self.tempdir) + binary = os.path.join(installdir, 'Contents', 'MacOS', 'firefox') + self.assertEqual(binary, mozinstall.get_binary(installdir, 'firefox')) + + def test_get_binary_error(self): + """ Test an InvalidBinary error is raised """ + + tempdir_empty = tempfile.mkdtemp() + self.assertRaises(mozinstall.InvalidBinary, mozinstall.get_binary, + tempdir_empty, 'firefox') + mozfile.rmtree(tempdir_empty) + + @unittest.skipIf(mozinfo.isWin, "Bug 1157352 - We need a new firefox.exe " + "for mozinstall 1.12 and higher.") + def test_is_installer(self): + """ Test we can identify a correct installer """ + + if mozinfo.isLinux: + self.assertTrue(mozinstall.is_installer(self.bz2)) + + if mozinfo.isWin: + # test zip installer + self.assertTrue(mozinstall.is_installer(self.zipfile)) + + # test exe installer + self.assertTrue(mozinstall.is_installer(self.exe)) + + try: + # test stub browser file + # without pefile on the system this test will fail + import pefile # noqa + stub_exe = os.path.join(here, 'build_stub', 'firefox.exe') + self.assertFalse(mozinstall.is_installer(stub_exe)) + except ImportError: + pass + + if mozinfo.isMac: + self.assertTrue(mozinstall.is_installer(self.dmg)) + + def test_invalid_source_error(self): + """ Test InvalidSource error is raised with an incorrect installer """ + + if mozinfo.isLinux: + self.assertRaises(mozinstall.InvalidSource, mozinstall.install, + self.dmg, 'firefox') + + elif mozinfo.isWin: + self.assertRaises(mozinstall.InvalidSource, mozinstall.install, + self.bz2, 'firefox') + + elif mozinfo.isMac: + self.assertRaises(mozinstall.InvalidSource, mozinstall.install, + self.bz2, 'firefox') + + @unittest.skipIf(mozinfo.isWin, "Bug 1157352 - We need a new firefox.exe " + "for mozinstall 1.12 and higher.") + def test_install(self): + """ Test mozinstall's install capability """ + + if mozinfo.isLinux: + installdir = mozinstall.install(self.bz2, self.tempdir) + self.assertEqual(os.path.join(self.tempdir, 'firefox'), installdir) + + elif mozinfo.isWin: + installdir_exe = mozinstall.install(self.exe, + os.path.join(self.tempdir, 'exe')) + self.assertEqual(os.path.join(self.tempdir, 'exe', 'firefox'), + installdir_exe) + + installdir_zip = mozinstall.install(self.zipfile, + os.path.join(self.tempdir, 'zip')) + self.assertEqual(os.path.join(self.tempdir, 'zip', 'firefox'), + installdir_zip) + + elif mozinfo.isMac: + installdir = mozinstall.install(self.dmg, self.tempdir) + self.assertEqual(os.path.join(os.path.realpath(self.tempdir), + 'FirefoxStub.app'), installdir) + + @unittest.skipIf(mozinfo.isWin, "Bug 1157352 - We need a new firefox.exe " + "for mozinstall 1.12 and higher.") + def test_uninstall(self): + """ Test mozinstall's uninstall capabilites """ + # Uninstall after installing + + if mozinfo.isLinux: + installdir = mozinstall.install(self.bz2, self.tempdir) + mozinstall.uninstall(installdir) + self.assertFalse(os.path.exists(installdir)) + + elif mozinfo.isWin: + # Exe installer for Windows + installdir_exe = mozinstall.install(self.exe, + os.path.join(self.tempdir, 'exe')) + mozinstall.uninstall(installdir_exe) + self.assertFalse(os.path.exists(installdir_exe)) + + # Zip installer for Windows + installdir_zip = mozinstall.install(self.zipfile, + os.path.join(self.tempdir, 'zip')) + mozinstall.uninstall(installdir_zip) + self.assertFalse(os.path.exists(installdir_zip)) + + elif mozinfo.isMac: + installdir = mozinstall.install(self.dmg, self.tempdir) + mozinstall.uninstall(installdir) + self.assertFalse(os.path.exists(installdir)) + +if __name__ == '__main__': + unittest.main() |