| # 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 argparse |
| import ConfigParser |
| from StringIO import StringIO |
| import os |
| import re |
| import sys |
| import tempfile |
| import xml.dom.minidom |
| import zipfile |
| |
| import mozfile |
| import mozlog |
| |
| import errors |
| |
| |
| INI_DATA_MAPPING = (('application', 'App'), ('platform', 'Build')) |
| |
| |
| class Version(object): |
| |
| def __init__(self): |
| self._info = {} |
| self._logger = mozlog.get_default_logger(component='mozversion') |
| if not self._logger: |
| self._logger = mozlog.unstructured.getLogger('mozversion') |
| |
| def get_gecko_info(self, path): |
| for type, section in INI_DATA_MAPPING: |
| config_file = os.path.join(path, "%s.ini" % type) |
| if os.path.exists(config_file): |
| self._parse_ini_file(open(config_file), type, section) |
| else: |
| self._logger.warning('Unable to find %s' % config_file) |
| |
| def _parse_ini_file(self, fp, type, section): |
| config = ConfigParser.RawConfigParser() |
| config.readfp(fp) |
| name_map = {'codename': 'display_name', |
| 'milestone': 'version', |
| 'sourcerepository': 'repository', |
| 'sourcestamp': 'changeset'} |
| for key, value in config.items(section): |
| name = name_map.get(key, key).lower() |
| self._info['%s_%s' % (type, name)] = config.has_option( |
| section, key) and config.get(section, key) or None |
| |
| if not self._info.get('application_display_name'): |
| self._info['application_display_name'] = \ |
| self._info.get('application_name') |
| |
| |
| class LocalFennecVersion(Version): |
| |
| def __init__(self, path, **kwargs): |
| Version.__init__(self, **kwargs) |
| self.get_gecko_info(path) |
| |
| def get_gecko_info(self, path): |
| archive = zipfile.ZipFile(path, 'r') |
| archive_list = archive.namelist() |
| for type, section in INI_DATA_MAPPING: |
| filename = "%s.ini" % type |
| if filename in archive_list: |
| self._parse_ini_file(archive.open(filename), type, |
| section) |
| else: |
| self._logger.warning('Unable to find %s' % filename) |
| |
| if "package-name.txt" in archive_list: |
| self._info["package_name"] = \ |
| archive.open("package-name.txt").readlines()[0].strip() |
| |
| |
| class LocalVersion(Version): |
| |
| def __init__(self, binary, **kwargs): |
| Version.__init__(self, **kwargs) |
| |
| if binary: |
| # on Windows, the binary may be specified with or without the |
| # .exe extension |
| if not os.path.exists(binary) and not os.path.exists(binary + |
| '.exe'): |
| raise IOError('Binary path does not exist: %s' % binary) |
| path = os.path.dirname(os.path.realpath(binary)) |
| else: |
| path = os.getcwd() |
| |
| if not self.check_location(path): |
| if sys.platform == 'darwin': |
| resources_path = os.path.join(os.path.dirname(path), |
| 'Resources') |
| if self.check_location(resources_path): |
| path = resources_path |
| else: |
| raise errors.LocalAppNotFoundError(path) |
| else: |
| raise errors.LocalAppNotFoundError(path) |
| |
| self.get_gecko_info(path) |
| |
| def check_location(self, path): |
| return (os.path.exists(os.path.join(path, 'application.ini')) |
| and os.path.exists(os.path.join(path, 'platform.ini'))) |
| |
| |
| class B2GVersion(Version): |
| |
| def __init__(self, sources=None, **kwargs): |
| Version.__init__(self, **kwargs) |
| |
| sources = sources or \ |
| os.path.exists(os.path.join(os.getcwd(), 'sources.xml')) and \ |
| os.path.join(os.getcwd(), 'sources.xml') |
| |
| if sources and os.path.exists(sources): |
| sources_xml = xml.dom.minidom.parse(sources) |
| for element in sources_xml.getElementsByTagName('project'): |
| path = element.getAttribute('path') |
| changeset = element.getAttribute('revision') |
| if path in ['gaia', 'gecko', 'build']: |
| if path == 'gaia' and self._info.get('gaia_changeset'): |
| break |
| self._info['_'.join([path, 'changeset'])] = changeset |
| |
| def get_gaia_info(self, app_zip): |
| tempdir = tempfile.mkdtemp() |
| try: |
| gaia_commit = os.path.join(tempdir, 'gaia_commit.txt') |
| try: |
| zip_file = zipfile.ZipFile(app_zip.name) |
| with open(gaia_commit, 'w') as f: |
| f.write(zip_file.read('resources/gaia_commit.txt')) |
| except zipfile.BadZipfile: |
| self._logger.info('Unable to unzip application.zip, falling ' |
| 'back to system unzip') |
| from subprocess import call |
| call(['unzip', '-j', app_zip.name, 'resources/gaia_commit.txt', |
| '-d', tempdir]) |
| |
| with open(gaia_commit) as f: |
| changeset, date = f.read().splitlines() |
| self._info['gaia_changeset'] = re.match( |
| '^\w{40}$', changeset) and changeset or None |
| self._info['gaia_date'] = date |
| except KeyError: |
| self._logger.warning( |
| 'Unable to find resources/gaia_commit.txt in ' |
| 'application.zip') |
| finally: |
| mozfile.remove(tempdir) |
| |
| |
| class LocalB2GVersion(B2GVersion): |
| |
| def __init__(self, binary, sources=None, **kwargs): |
| B2GVersion.__init__(self, sources, **kwargs) |
| |
| if binary: |
| if not os.path.exists(binary): |
| raise IOError('Binary path does not exist: %s' % binary) |
| path = os.path.dirname(binary) |
| else: |
| if os.path.exists(os.path.join(os.getcwd(), 'application.ini')): |
| path = os.getcwd() |
| |
| self.get_gecko_info(path) |
| |
| zip_path = os.path.join( |
| path, 'gaia', 'profile', 'webapps', |
| 'settings.gaiamobile.org', 'application.zip') |
| if os.path.exists(zip_path): |
| with open(zip_path, 'rb') as zip_file: |
| self.get_gaia_info(zip_file) |
| else: |
| self._logger.warning('Error pulling gaia file') |
| |
| |
| class RemoteB2GVersion(B2GVersion): |
| |
| def __init__(self, sources=None, dm_type='adb', host=None, |
| device_serial=None, adb_host=None, adb_port=None, |
| **kwargs): |
| B2GVersion.__init__(self, sources, **kwargs) |
| |
| try: |
| import mozdevice |
| except ImportError: |
| self._logger.critical("mozdevice is required to get the version" |
| " of a remote device") |
| raise |
| |
| if dm_type == 'adb': |
| dm = mozdevice.DeviceManagerADB(deviceSerial=device_serial, |
| serverHost=adb_host, |
| serverPort=adb_port) |
| elif dm_type == 'sut': |
| if not host: |
| raise errors.RemoteAppNotFoundError( |
| 'A host for SUT must be supplied.') |
| dm = mozdevice.DeviceManagerSUT(host=host) |
| else: |
| raise errors.RemoteAppNotFoundError( |
| 'Unknown device manager type: %s' % dm_type) |
| |
| if not sources: |
| path = 'system/sources.xml' |
| if dm.fileExists(path): |
| sources = StringIO(dm.pullFile(path)) |
| else: |
| self._logger.info('Unable to find %s' % path) |
| |
| tempdir = tempfile.mkdtemp() |
| for ini in ('application', 'platform'): |
| with open(os.path.join(tempdir, '%s.ini' % ini), 'w') as f: |
| f.write(dm.pullFile('/system/b2g/%s.ini' % ini)) |
| f.flush() |
| self.get_gecko_info(tempdir) |
| mozfile.remove(tempdir) |
| |
| for path in ['/system/b2g', '/data/local']: |
| path += '/webapps/settings.gaiamobile.org/application.zip' |
| if dm.fileExists(path): |
| with tempfile.NamedTemporaryFile() as f: |
| dm.getFile(path, f.name) |
| self.get_gaia_info(f) |
| break |
| else: |
| self._logger.warning('Error pulling gaia file') |
| |
| build_props = dm.pullFile('/system/build.prop') |
| desired_props = { |
| 'ro.build.version.incremental': 'device_firmware_version_incremental', |
| 'ro.build.version.release': 'device_firmware_version_release', |
| 'ro.build.date.utc': 'device_firmware_date', |
| 'ro.product.device': 'device_id'} |
| for line in build_props.split('\n'): |
| if not line.strip().startswith('#') and '=' in line: |
| key, value = [s.strip() for s in line.split('=', 1)] |
| if key in desired_props.keys(): |
| self._info[desired_props[key]] = value |
| |
| if self._info.get('device_id', '').lower() == 'flame': |
| for prop in ['ro.boot.bootloader', 't2m.sw.version']: |
| value = dm.shellCheckOutput(['getprop', prop]) |
| if value: |
| self._info['device_firmware_version_base'] = value |
| break |
| |
| |
| def get_version(binary=None, sources=None, dm_type=None, host=None, |
| device_serial=None, adb_host=None, adb_port=None): |
| """ |
| Returns the application version information as a dict. You can specify |
| a path to the binary of the application or an Android APK file (to get |
| version information for Firefox for Android). If this is omitted then the |
| current directory is checked for the existance of an application.ini |
| file. If not found and that the binary path was not specified, then it is |
| assumed the target application is a remote Firefox OS instance. |
| |
| :param binary: Path to the binary for the application or Android APK file |
| :param sources: Path to the sources.xml file (Firefox OS) |
| :param dm_type: Device manager type. Must be 'adb' or 'sut' (Firefox OS) |
| :param host: Host address of remote Firefox OS instance (SUT) |
| :param device_serial: Serial identifier of Firefox OS device (ADB) |
| :param adb_host: Host address of ADB server |
| :param adb_port: Port of ADB server |
| """ |
| try: |
| if binary and zipfile.is_zipfile(binary) and 'AndroidManifest.xml' in \ |
| zipfile.ZipFile(binary, 'r').namelist(): |
| version = LocalFennecVersion(binary) |
| else: |
| version = LocalVersion(binary) |
| if version._info.get('application_name') == 'B2G': |
| version = LocalB2GVersion(binary, sources=sources) |
| except errors.LocalAppNotFoundError: |
| if binary: |
| # we had a binary argument, do not search for remote B2G |
| raise |
| version = RemoteB2GVersion(sources=sources, |
| dm_type=dm_type, |
| host=host, |
| adb_host=adb_host, |
| adb_port=adb_port, |
| device_serial=device_serial) |
| |
| for (key, value) in sorted(version._info.items()): |
| if value: |
| version._logger.info('%s: %s' % (key, value)) |
| |
| return version._info |
| |
| |
| def cli(args=sys.argv[1:]): |
| parser = argparse.ArgumentParser( |
| description='Display version information for Mozilla applications') |
| parser.add_argument( |
| '--binary', |
| help='path to application binary or apk') |
| fxos = parser.add_argument_group('Firefox OS') |
| fxos.add_argument( |
| '--sources', |
| help='path to sources.xml') |
| fxos.add_argument( |
| '--device', |
| help='serial identifier of device to target') |
| fxos.add_argument( |
| '--adb-host', |
| help='host running adb') |
| fxos.add_argument( |
| '--adb-port', |
| help='port running adb') |
| mozlog.commandline.add_logging_group( |
| parser, |
| include_formatters=mozlog.commandline.TEXT_FORMATTERS |
| ) |
| |
| args = parser.parse_args() |
| dm_type = os.environ.get('DM_TRANS', 'adb') |
| host = os.environ.get('TEST_DEVICE') |
| |
| mozlog.commandline.setup_logging( |
| 'mozversion', args, {'mach': sys.stdout}) |
| |
| get_version(binary=args.binary, |
| sources=args.sources, |
| dm_type=dm_type, |
| host=host, |
| device_serial=args.device, |
| adb_host=args.adb_host, |
| adb_port=args.adb_port) |
| |
| if __name__ == '__main__': |
| cli() |