summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozinstall
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozinstall')
-rw-r--r--testing/mozbase/mozinstall/mozinstall/__init__.py6
-rwxr-xr-xtesting/mozbase/mozinstall/mozinstall/mozinstall.py342
-rw-r--r--testing/mozbase/mozinstall/setup.py53
-rw-r--r--testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmgbin0 -> 13441 bytes
-rw-r--r--testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2bin0 -> 2882 bytes
-rw-r--r--testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zipbin0 -> 8707 bytes
-rw-r--r--testing/mozbase/mozinstall/tests/manifest.ini1
-rw-r--r--testing/mozbase/mozinstall/tests/test.py169
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
new file mode 100644
index 000000000..f7f36f631
--- /dev/null
+++ b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg
Binary files differ
diff --git a/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2 b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2
new file mode 100644
index 000000000..cb046a0e7
--- /dev/null
+++ b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2
Binary files differ
diff --git a/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip
new file mode 100644
index 000000000..7c3f61a5e
--- /dev/null
+++ b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip
Binary files differ
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()