diff --git a/README.md b/README.md index 224ac4b..a85a8de 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # ff-tool Python CLI tool for downloading desktop Firefox version, managing profiles and test prefs + +# Work in progress... DO NOT USE diff --git a/firefox_download.py b/firefox_download.py new file mode 100644 index 0000000..ec324af --- /dev/null +++ b/firefox_download.py @@ -0,0 +1,52 @@ +""" +Module to download OS-specific versions of Firefox: +1. General Release (gr) +2. Beta (beta) +3. Developer Edition (aurora) +4. Nightly (nightly) +""" + +from firefox_env_handler import IniHandler +from mozdownload import FactoryScraper + + +CONFIG_CHANNELS = 'configs/channels.ini' + +try: + import configparser # Python 3 +except: + import ConfigParser as configparser # Python 2 + +config = configparser.ConfigParser() +config.read(CONFIG_CHANNELS) +env = IniHandler() +env.load_os_config('configs') + + +def set_channel(channel): + ch_version = config.get(channel, 'version') + ch_type = config.get(channel, 'type') + ch_branch = config.get(channel, 'branch') + return ch_version, ch_type, ch_branch + + +def download(channel): + print('USING CHANNEL: {0}'.format(channel)) + v, t, b = set_channel(channel) + download_filename = env.get(channel, 'DOWNLOAD_FILENAME') + scraper = FactoryScraper( + t, + version=v, + branch=b, + destination='_temp/{0}'.format(download_filename) + ) + scraper.download() + + +def main(): + for channel in config.sections(): + download(channel) + + +if __name__ == '__main__': + main() diff --git a/firefox_env_handler.py b/firefox_env_handler.py new file mode 100755 index 0000000..798ef8f --- /dev/null +++ b/firefox_env_handler.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python + +import os +import platform +import re +import shutil +import sys +import configparser + + +class FirefoxEnvHandler(): + LINUX = 'linux' + MAC = 'darwin' + WINDOWS = 'cygwin' + + def __init__(self): + pass + + @staticmethod + def get_os(): + """ + Do our best to determine the user's current operating system. + """ + system = platform.system().lower() + return re.split('[-_]', system, maxsplit=1).pop(0) + + @classmethod + def is_linux(cls): + """ + Am I running on Linux? + """ + return cls.get_os() == cls.LINUX + + @classmethod + def is_mac(cls): + """ + Am I running on Mac/Darwin? + """ + return cls.get_os() == cls.MAC + + @classmethod + def is_other(cls): + """ + I 'literally' have no idea who you are. + """ + return not (cls.is_linux() or cls.is_mac() or cls.is_windows()) + + @classmethod + def is_windows(cls): + """ + Am I running on Windows/Cygwin? + """ + return cls.get_os() == cls.WINDOWS + + @staticmethod + def banner(str, length=79, delimiter='='): + """ + Throws up a debug header to make output subjectively "easier" to read + in stdout. + """ + if length <= 0: + length = len(str) + + divider = delimiter * length + output = '\n'.join(['', divider, str, divider]) + print(output) + + @staticmethod + def clean_folder(path, foot_gun=True): + """ + Recursively delete the specified path. + """ + if os.path.isdir(path): + print(('Deleting {0}'.format(path))) + shutil.rmtree(path) + + else: + print(('{0} is not a directory'.format(path))) + + +class IniHandler(FirefoxEnvHandler): + def __init__(self, ini_path=None): + """ + Creates a new config parser object, and optionally loads a config file + if `ini_path` was specified. + """ + self.config = configparser.SafeConfigParser(os.environ) + + if ini_path is not None: + self.load_config(ini_path) + + def load_config(self, ini_path): + """ + Load an INI config based on the specified `ini_path`. + """ + IniHandler.banner('LOADING {0}'.format(ini_path), 79, '-') + + # Make sure the specified config file exists, fail hard if missing. + if not os.path.isfile(ini_path): + sys.exit('Config file not found: {0}'.format(ini_path)) + + self.config.read(ini_path) + + def load_os_config(self, config_path): + """ + Load an INI file based on the current operating system. This method + will attempt to load a "darwin.ini", "cygwin.ini", or "linux-gnu.ini" + file from the specified `config_path` based on the current OS. + """ + os_config = os.path.join(config_path, IniHandler.get_os() + '.ini') + self.load_config(os_config) + + def create_env_file(self, out_file='.env'): + """ + Generate and save the output environment file so we can source it from + something like .bashrc or .bashprofile. + """ + IniHandler.banner('CREATING ENV FILE ({0})'.format(out_file)) + + env_fmt = "export %s=\"%s\"" + env_vars = [] + + # Generic paths to Sikuli and Firefox profile directories. + for key in ['PATH_SIKULIX_BIN', 'PATH_FIREFOX_PROFILES']: + env_key = self.get_default(key + '_ENV') + env_vars.append(env_fmt % (key, env_key)) + + # Channel specific Firefox binary paths. + for channel in self.sections(): + export_name = 'PATH_FIREFOX_APP_' + channel.upper() + firefox_bin = self.get(channel, 'PATH_FIREFOX_BIN_ENV') + env_vars.append(env_fmt % (export_name, firefox_bin)) + + output = '\n'.join(env_vars) + '\n' + print(output) + + with open(out_file, 'w') as env_file: + env_file.write(output) + env_file.close() + + def sections(self): + """ + Shortcut for getting a config's `sections()` array without needing to + do the visually horrifying `self.config.config.sections()`. + """ + return self.config.sections() + + def get(self, section, option): + """ + Shortcut for calling a config's `get()` method without needing to + do the visually horrifying `self.config.config.get()`. + """ + return self.config.get(section, option) + + def set(self, section, option, value): + """ + Shortcut for calling a config's `set()` method without needing to + do the visually horrifying `self.config.config.set()`. + """ + return self.config.set(section, option, str(value)) + + def get_default(self, option): + """ + Shortcut to get a value from the "DEFAULTS" section of a ConfigParser + INI file. Doesn't save much time, but reads a bit easier. + """ + return self.get('DEFAULT', option) + + +if __name__ == '__main__': + + i = IniHandler() + i.load_os_config('configs') + i.create_env_file() diff --git a/firefox_install.py b/firefox_install.py new file mode 100755 index 0000000..8dbfe62 --- /dev/null +++ b/firefox_install.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python + +from firefox_env_handler import IniHandler +from fabric.api import local + +import os +import sys + + +class FirefoxInstall(object): + def __init__(self, config, archive_dir='temp'): + self.CACHE_FILE = 'cache.ini' + self.out_dir = archive_dir + self.cache_path = os.path.join(self.out_dir, self.CACHE_FILE) + self.cache = IniHandler(self.cache_path) + + # Do some basic type checking on the `config` attribute. + if isinstance(config, IniHandler): + self.config = config + + elif isinstance(config, str): + self.config = IniHandler() + self.config.load_os_config(config) + + else: + sys.exit('FirefoxInstall: Unexpected config data type') + + def install_all(self, force=False): + IniHandler.banner('INSTALLING FIREFOXES') + for channel in self.config.sections(): + self.install_channel(channel, force) + + def install_channel(self, channel, force=False): + was_cached = self.cache.config.getboolean('cached', channel) + filename = self.config.get(channel, 'DOWNLOAD_FILENAME') + install_dir = self.config.get(channel, 'PATH_FIREFOX_APP') + installer = os.path.join('.', self.out_dir, filename) + + if force or not was_cached: + print(('Installing {0}'.format(channel))) + + if IniHandler.is_linux(): + # TODO: Move to /opt/* and chmod file? + # `tar -jxf firefox-beta.tar.gz -C ./beta --strip-components=1`? + local('tar -jxf {0} && mv firefox {1}'.format(installer, install_dir)) + + elif IniHandler.is_windows(): + local('{0} -ms'.format(installer)) + + if channel == 'beta': + # Since Beta and General Release channels install to the same directory, + # install Beta first then rename the directory. + gr_install_dir = self.config.get('gr', 'PATH_FIREFOX_APP') + local('mv "{0}" "{1}"'.format(gr_install_dir, install_dir)) + + elif IniHandler.is_mac(): + # TODO: Mount the DMG to /Volumes and copy to /Applications? + print('Do something...') + + else: + print(('[{0}] was cached, skipping install.'.format(channel))) + + local('"{0}" --version # {1}'.format(self.config.get(channel, 'PATH_FIREFOX_BIN_ENV'), channel)) + + +def main(): + ff_install = FirefoxInstall('./configs/') + ff_install.install_all(True) + + +if __name__ == '__main__': + main() diff --git a/firefox_profile.py b/firefox_profile.py new file mode 100644 index 0000000..427f196 --- /dev/null +++ b/firefox_profile.py @@ -0,0 +1,96 @@ +""" +This module uses Fabric API to generate a Firefox Profile by concatenating the +following preferences files: +- ./_utils/prefs.ini +- .//prefs.ini +- .///prefs.ini + +The profile is then created using the specified name and saved to the ../_temp/ +directory. +""" + +try: + import configparser # Python 3 +except: + import ConfigParser as configparser # Python 2 + +import os +import shutil + +import configargparse +from fabric.api import local # It looks like Fabric may only support Python 2. + +PATH_PROJECT = os.path.abspath('../') +PATH_TEMP = os.path.join(PATH_PROJECT, '_temp') +FILE_PREFS = 'prefs.ini' + +config = configparser.ConfigParser() + + +def _parse_args(): + """Parses out args for CLI""" + parser = configargparse.ArgumentParser( + description='CLI tool for creating Firefox profiles via mozprofile CLI') + parser.add_argument('-a', '--application', + required=True, + help='Application to test. Example: "loop-server"') + parser.add_argument('-t', '--test-type', + required=True, + help='Application test type. Example: "stack-check"') + parser.add_argument('-e', '--env', + help='Test environment. Example: "dev", "stage", ...') + parser.add_argument('-p', '--profile', + required=True, + help='Profile name.') + + args = parser.parse_args() + return args, parser + + +def prefs_paths(application, test_type, env='stage'): + path_global = os.path.join(PATH_PROJECT, '_utils', FILE_PREFS) + path_app_dir = os.path.join(PATH_PROJECT, application) + path_app = os.path.join(path_app_dir, FILE_PREFS) + path_app_test_type = os.path.join(path_app_dir, test_type, FILE_PREFS) + + valid_paths = [path_global] + + if os.path.exists(path_app): + config.read(path_app) + # Make sure the specified INI file has the specified section. + if config.has_section(env): + valid_paths.append(path_app + ":" + env) + + if os.path.exists(path_app_test_type): + config.read(path_app_test_type) + if config.has_section(env): + valid_paths.append(path_app_test_type + ":" + env) + + return valid_paths + + +def create_mozprofile(application, test_type, env, profile_dir): + full_profile_dir = os.path.join(PATH_TEMP, profile_dir) + + # If temp profile already exists, kill it so it doesn't merge unexpectedly. + if os.path.exists(full_profile_dir): + print("Deleting existing profile... {0}".format(full_profile_dir)) + shutil.rmtree(full_profile_dir) + + cmd = [ + 'mozprofile', + '--profile={0}'.format(full_profile_dir) + ] + for path in prefs_paths(application, test_type, env): + cmd.append("--preferences=" + path) + + local(" ".join(cmd)) + + +def main(): + args, parser = _parse_args() + create_mozprofile(args.application, args.test_type, args.env, args.profile) + + +if __name__ == '__main__': + main() diff --git a/firefox_profile_handler.py b/firefox_profile_handler.py new file mode 100755 index 0000000..c197a83 --- /dev/null +++ b/firefox_profile_handler.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +from firefox_env_handler import FirefoxEnvHandler, IniHandler + +import sys + + +class FirefoxProfileHandler(object): + + def __init__(self, config): + # Do some basic type checking on the `config` attribute. + if isinstance(config, IniHandler): + self.config = config + + elif isinstance(config, str): + self.config = IniHandler() + self.config.load_os_config(config) + + else: + sys.exit('FirefoxProfileHandler: Unexpected config data type') + + self.profile_dir = self.config.get_default('PATH_FIREFOX_PROFILES_ENV') + + def switch_prefs(self, profile_name, user_prefs, channel='nightly'): + channel_firefox_bin = self.config.get(channel, 'PATH_FIREFOX_BIN_ENV') + print(('{0} -CreateProfile {1}'.format(channel_firefox_bin, profile_name))) + print('copy prefs.js to dir') + + def delete_all_profiles(self): + FirefoxEnvHandler.clean_folder(self.profile_dir) + + +def main(): + config_path = './configs/' + FirefoxProfileHandler(config_path) + + +if __name__ == '__main__': + main() diff --git a/firefox_tool.py b/firefox_tool.py new file mode 100644 index 0000000..6692e36 --- /dev/null +++ b/firefox_tool.py @@ -0,0 +1,45 @@ +"""firefox test tool helper script""" +import glob +import configargparse + + +def _parse_args(): + """Parses out args for CLI""" + parser = configargparse.ArgumentParser( + description='cross-platform CLI tool for installing firefox and managing profiles') + parser.add_argument('-i', '--install', + help='install firefox version (release, beta, aurora, nightly', + default='nightly', + type=str) + parser.add_argument('-u', '--uninstall', + help='install firefox version (release, beta, aurora, nightly, ALL', + type=str) + parser.add_argument('-p', '--create-profile', + help='create new profile (indicate name)', + type=str) + parser.add_argument('-d', '--delete-profile', + help='delete profile (indicate name)', + type=str) + parser.add_argument('-s', '--set-profile-path', + help='-s ', + default='', + type=str) + + args = parser.parse_args() + return args, parser + + +def main(): + """Main entrypoint for CLI""" + + args, parser = _parse_args() + + print('INSTALL: {0}'.format(args.install)) + print('UNINSTALL: {0}'.format(args.uninstall)) + print('CREATE PROFILE: {0}'.format(args.create_profile)) + print('DELETE PROFILE: {0}'.format(args.delete_profile)) + print('SET PROFILE PATH: {0}'.format(args.set_profile_path)) + + +if __name__ == '__main__': + main() diff --git a/firefox_uninstall.py b/firefox_uninstall.py new file mode 100755 index 0000000..204a33b --- /dev/null +++ b/firefox_uninstall.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +from firefox_env_handler import IniHandler +from fabric.api import local + +import os +import sys + + +class FirefoxUninstall(object): + def __init__(self, config, archive_dir="temp"): + self.CACHE_FILE = "cache.ini" + self.out_dir = archive_dir + self.cache_path = os.path.join(self.out_dir, self.CACHE_FILE) + self.cache = IniHandler(self.cache_path) + + # Do some basic type checking on the `config` attribute. + if isinstance(config, IniHandler): + self.config = config + elif isinstance(config, str): + self.config = IniHandler() + self.config.load_os_config(config) + else: + sys.exit("FirefoxUninstall: Unexpected config data type") + + def uninstall_all(self, force=False): + """ + Delete all the Firefox apps (nightly, aurora, beta, general release), + and then delete the shared profiles directory. + """ + IniHandler.banner("UNINSTALLING FIREFOXES") + + for channel in self.config.sections(): + self.uninstall_channel(channel, force) + + def uninstall_channel(self, channel, force=False): + was_cached = self.cache.config.getboolean("cached", channel) + + if force or not was_cached: + path_firefox_app = self.config.get(channel, "PATH_FIREFOX_APP") + if not os.path.isdir(path_firefox_app): + print(('Firefox not found: {0}'.format(path_firefox_app))) + return + + # If we're on Windows/Cygwin, use the uninstaller. + if self.config.is_windows(): + local("\"{0}/uninstall/helper.exe\" -ms".format(path_firefox_app)) + + # Otherwise just rimraf the Firefox folder. + else: + IniHandler.clean_folder(path_firefox_app, False) + + else: + print(("[%s] was cached, skipping uninstall." % (channel))) + + +def main(): + config_path = "./configs/" + ff_uninstall = FirefoxUninstall(config_path) + ff_uninstall.uninstall_all() + +if __name__ == '__main__': + main()