diff --git a/tests/data/testframeworks/baseinstallerfake.py b/tests/data/testframeworks/baseinstallerfake.py index 4b17f253..0f008941 100644 --- a/tests/data/testframeworks/baseinstallerfake.py +++ b/tests/data/testframeworks/baseinstallerfake.py @@ -82,13 +82,19 @@ def __init__(self, **kwargs): checksum_type=ChecksumType.sha1, dir_to_decompress_in_tarball="base-framework-*", desktop_filename="base-framework.desktop", - required_files_path=[os.path.join("bin", "studio.sh")], **kwargs) + required_files_path=[os.path.join("bin", "studio.sh")], + updatable=True, **kwargs) arch = platform.machine() self.tag = 'id="linux-bundle64"' if arch == 'i686': self.tag = 'id="linux-bundle32"' + update_parse = ">android-studio-ide-(.*)-linux.zip" + + def get_version(self): + return "135.1641136" + def parse_license(self, line, license_txt, in_license): """Parse download page for license""" if line.startswith('
'): diff --git a/tests/large/test_baseinstaller.py b/tests/large/test_baseinstaller.py index 20d04c07..7cb47551 100644 --- a/tests/large/test_baseinstaller.py +++ b/tests/large/test_baseinstaller.py @@ -536,3 +536,38 @@ def test_download_page_404(self): # we have nothing installed self.assertFalse(self.launcher_exists_and_is_pinned(self.desktop_filename)) + + def test_update_not_available(self): + for loop in ("install", "update"): + if loop == "update": + pass + self.child = spawn_process(self.command('{} base base-framework --update'.format(UMAKE))) + self.expect_and_no_warn("\[.*\] ") + self.child.sendline("a") + self.expect_and_no_warn("Framework Base Framework already up to date", + timeout=self.TIMEOUT_INSTALL_PROGRESS) + else: + self.child = spawn_process(self.command('{} base base-framework'.format(UMAKE))) + self.expect_and_no_warn("Choose installation path: {}".format(self.installed_path)) + self.child.sendline("") + self.expect_and_no_warn("\[.*\] ") + self.child.sendline("a") + self.expect_and_no_warn("Installation done", timeout=self.TIMEOUT_INSTALL_PROGRESS) + self.wait_and_close() + + def test_update_available(self): + self.child = spawn_process(self.command('{} base base-framework'.format(UMAKE))) + self.expect_and_no_warn("Choose installation path: {}".format(self.installed_path)) + self.child.sendline("") + self.expect_and_no_warn("\[.*\] ") + self.child.sendline("a") + self.expect_and_no_warn("Installation done", timeout=self.TIMEOUT_INSTALL_PROGRESS) + self.wait_and_close() + with swap_file_and_restore(self.download_page_file_path) as content: + with open(self.download_page_file_path, "w") as newfile: + newfile.write(content.replace('135.1641136-linux.zip', "136.123-linux.zip")) + self.child = spawn_process(self.command('{} base base-framework --update'.format(UMAKE))) + self.expect_and_no_warn("\[.*\] ") + self.child.sendline("a") + self.expect_and_no_warn("Installation done", timeout=self.TIMEOUT_INSTALL_PROGRESS) + self.wait_and_close() diff --git a/tests/small/test_frameworks_loader.py b/tests/small/test_frameworks_loader.py index e4842aa7..49b1ce1b 100644 --- a/tests/small/test_frameworks_loader.py +++ b/tests/small/test_frameworks_loader.py @@ -297,12 +297,13 @@ def test_parse_category_and_framework_run_correct_framework(self): args.framework = "framework-b" args.accept_license = False args.remove = False + args.update = False with patch.object(self.CategoryHandler.categories[args.category].frameworks["framework-b"], "setup")\ as setup_call: self.CategoryHandler.categories[args.category].run_for(args) self.assertTrue(setup_call.called) - self.assertEqual(setup_call.call_args, call(install_path=None, auto_accept_license=False)) + self.assertEqual(setup_call.call_args, call(install_path=None, auto_accept_license=False, update=False)) def test_parse_no_framework_run_default_for_category(self): """Parsing category will run default framework""" @@ -312,11 +313,12 @@ def test_parse_no_framework_run_default_for_category(self): args.framework = None args.accept_license = False args.remove = False + args.update = False with patch.object(self.CategoryHandler.categories[args.category].frameworks["framework-a"], "setup")\ as setup_call: self.CategoryHandler.categories[args.category].run_for(args) self.assertTrue(setup_call.called) - self.assertEqual(setup_call.call_args, call(install_path=None, auto_accept_license=False)) + self.assertEqual(setup_call.call_args, call(install_path=None, auto_accept_license=False, update=False)) def test_parse_category_and_framework_run_correct_remove_framework(self): """Parsing category and framework with --remove run remove on right category and framework""" @@ -370,7 +372,6 @@ def test_parse_category_and_framework_cannot_run_remove_with_destdir_framework(s def test_parse_category_and_framework_cannot_install_not_installable_but_installed_framework(self): """We cannot install frameworks that are not installable but already installed (and so, registered)""" - self.expect_warn_error = True args = Mock() args.category = "category-r" args.destdir = None @@ -402,12 +403,13 @@ def test_parse_category_and_framework_get_accept_license_arg(self): args.framework = "framework-b" args.accept_license = True args.remove = False + args.update = False with patch.object(self.CategoryHandler.categories[args.category].frameworks["framework-b"], "setup")\ as setup_call: self.CategoryHandler.categories[args.category].run_for(args) self.assertTrue(setup_call.called) - self.assertEqual(setup_call.call_args, call(install_path=None, auto_accept_license=True)) + self.assertEqual(setup_call.call_args, call(install_path=None, auto_accept_license=True, update=False)) def test_uninstantiable_framework(self): """A uninstantiable framework isn't loaded""" diff --git a/umake/__init__.py b/umake/__init__.py index 6bde44e8..2e48a33f 100644 --- a/umake/__init__.py +++ b/umake/__init__.py @@ -123,6 +123,7 @@ def main(): add_help=False) parser.add_argument('--help', action=_HelpAction, help=_('Show this help')) # add custom help parser.add_argument("-v", "--verbose", action="count", default=0, help=_("Increase output verbosity (2 levels)")) + parser.add_argument("--update", action="store_true", help=_("Update installed frameworks (that support update)")) parser.add_argument('-r', '--remove', action="store_true", help=_("Remove specified framework if installed")) diff --git a/umake/frameworks/__init__.py b/umake/frameworks/__init__.py index 01248581..173f0a52 100644 --- a/umake/frameworks/__init__.py +++ b/umake/frameworks/__init__.py @@ -139,7 +139,7 @@ class BaseFramework(metaclass=abc.ABCMeta): def __init__(self, name, description, category, force_loading=False, logo_path=None, is_category_default=False, install_path_dir=None, only_on_archs=None, only_ubuntu_version=None, packages_requirements=None, - only_for_removal=False, expect_license=False, need_root_access=False): + only_for_removal=False, expect_license=False, need_root_access=False, updatable=False): self.name = name self.description = description self.logo_path = None @@ -151,6 +151,7 @@ def __init__(self, name, description, category, force_loading=False, logo_path=N self.packages_requirements.extend(self.category.packages_requirements) self.only_for_removal = only_for_removal self.expect_license = expect_license + self.updatable = updatable # don't detect anything for completion mode (as we need to be quick), so avoid opening apt cache and detect # if it's installed. @@ -260,6 +261,10 @@ def remove(self): logger.error(_("You can't remove {} as it isn't installed".format(self.name))) UI.return_main_screen(status_code=2) + def version(self): + """Method call to get the verison for the current framework""" + pass + def mark_in_config(self): """Mark the installation as installed in the config file""" config = ConfigHandler().config @@ -290,6 +295,10 @@ def install_framework_parser(self, parser): "destdir should contain a /")) this_framework_parser.add_argument('-r', '--remove', action="store_true", help=_("Remove framework if installed")) + this_framework_parser.add_argument('--version', action="store_true", + help=_("Print the framework version if installed")) + this_framework_parser.add_argument('--update', action="store_true", + help=_("Update the framework if installed and possible")) if self.expect_license: this_framework_parser.add_argument('--accept-license', dest="accept_license", action="store_true", help=_("Accept license without prompting")) @@ -298,6 +307,12 @@ def install_framework_parser(self, parser): def run_for(self, args): """Running commands from args namespace""" logger.debug("Call run_for on {}".format(self.name)) + if args.version: + if args.destdir: + message = "You can't specify a destination dir while getting the version of a framework" + logger.error(message) + UI.return_main_screen(status_code=2) + self.version() if args.remove: if args.destdir: message = "You can't specify a destination dir while removing a framework" @@ -311,7 +326,7 @@ def run_for(self, args): install_path = os.path.abspath(os.path.expanduser(args.destdir)) if self.expect_license and args.accept_license: auto_accept_license = True - self.setup(install_path=install_path, auto_accept_license=auto_accept_license) + self.setup(install_path=install_path, auto_accept_license=auto_accept_license, update=args.update) class MainCategory(BaseCategory): @@ -367,6 +382,7 @@ def list_frameworks(): 'is_installable': True or False 'is_category_default': True or False 'only_for_removal': True or False + 'updatable': True or False }, ] }, @@ -383,7 +399,8 @@ def list_frameworks(): "is_installed": framework.is_installed, "is_installable": framework.is_installable, "is_category_default": framework.is_category_default, - "only_for_removal": framework.only_for_removal + "only_for_removal": framework.only_for_removal, + "updatable": framework.updatable } frameworks_dict.append(new_fram) diff --git a/umake/frameworks/baseinstaller.py b/umake/frameworks/baseinstaller.py index 5b5ccb02..808fdc98 100644 --- a/umake/frameworks/baseinstaller.py +++ b/umake/frameworks/baseinstaller.py @@ -26,7 +26,9 @@ import logging from progressbar import ProgressBar import os +import re import shutil +import subprocess import umake.frameworks from umake.decompressor import Decompressor from umake.interactions import InputText, YesNo, LicenseAgreement, DisplayMessage, UnknownProgress @@ -65,6 +67,7 @@ def __init__(self, *args, **kwargs): self.desktop_filename = kwargs.get("desktop_filename", None) self.icon_filename = kwargs.get("icon_filename", None) self.match_last_link = kwargs.get("match_last_link", False) + self.updatable = kwargs.get("updatable", False) for extra_arg in ["download_page", "checksum_type", "dir_to_decompress_in_tarball", "desktop_filename", "icon_filename", "required_files_path", "match_last_link"]: @@ -97,13 +100,24 @@ def is_installed(self): logger.debug("{} is installed".format(self.name)) return True - def setup(self, install_path=None, auto_accept_license=False): + def setup(self, install_path=None, auto_accept_license=False, update=False): self.arg_install_path = install_path self.auto_accept_license = auto_accept_license + self.update = update super().setup() # first step, check if installed - if self.is_installed: + if self.update and self.updatable: + if not self.is_installed: + UI.display(DisplayMessage("The framework {} is not installed".format(self.name))) + UI.return_main_screen() + try: + self.set_exec_path() + self.download_provider_page() + except: + UI.display(DisplayMessage("The framework {} is not configured to update manually".format(self.name))) + UI.return_main_screen() + elif self.is_installed: UI.display(YesNo("{} is already installed on your system, do you want to reinstall " "it anyway?".format(self.name), self.reinstall, UI.return_main_screen)) else: @@ -115,6 +129,42 @@ def reinstall(self): self.confirm_path(self.arg_install_path) remove_framework_envs_from_user(self.name) + def version(self): + super().version() + if not self.is_installed: + logger.error(_("You can't get the version for {} as it isn't installed".format(self.name))) + UI.return_main_screen(status_code=2) + try: + UI.display(DisplayMessage(self.get_version())) + except AttributeError as e: + logger.error("Version parse not implememted") + UI.return_main_screen() + + def get_version(self): + return re.search(self.version_parse['regex'], + subprocess.check_output(self.version_parse['command'].split()).decode()).group(1) + + @MainLoop.in_mainloop_thread + def run_update(self, result): + upstream_version = self.get_upstream_version(result) + if upstream_version and self.get_version() != upstream_version: + logger.debug("Running update of framework {}".format(self.name)) + self.arg_install_path = self.install_path + self.reinstall() + DownloadCenter([DownloadItem(self.download_page)], self.get_metadata_and_check_license, download=False) + UI.display(DisplayMessage("Framework {} update started".format(self.name))) + else: + logger.debug("No update available") + UI.display(DisplayMessage("Framework {} already up to date".format(self.name))) + UI.return_main_screen() + + def get_upstream_version(self, result): + for line in result[self.download_page].buffer: + line_content = line.decode() + parsed = re.search(self.update_parse, line_content) + if parsed: + return parsed.group(1) + def remove(self): """Remove current framework if installed @@ -166,7 +216,8 @@ def confirm_path(self, path_dir=""): return self.install_path = path_dir self.set_exec_path() - self.download_provider_page() + if not self.update: + self.download_provider_page() def set_installdir_to_clean(self): logger.debug("Mark non empty new installation path for cleaning.") @@ -174,9 +225,13 @@ def set_installdir_to_clean(self): self.set_exec_path() self.download_provider_page() - def download_provider_page(self): + def download_provider_page(self, update=False): logger.debug("Download application provider page") - DownloadCenter([DownloadItem(self.download_page)], self.get_metadata_and_check_license, download=False) + if self.update: + logger.debug("Check provider page for update") + DownloadCenter([DownloadItem(self.download_page)], self.run_update, download=False) + else: + DownloadCenter([DownloadItem(self.download_page)], self.get_metadata_and_check_license, download=False) def parse_license(self, line, license_txt, in_license): """Parse license per line, eventually write to license_txt if it's in the license part. diff --git a/umake/frameworks/go.py b/umake/frameworks/go.py index 9992968b..5e902889 100644 --- a/umake/frameworks/go.py +++ b/umake/frameworks/go.py @@ -49,7 +49,10 @@ def __init__(self, **kwargs): checksum_type=ChecksumType.sha256, dir_to_decompress_in_tarball="go", required_files_path=[os.path.join("bin", "go")], - **kwargs) + updatable=True, **kwargs) + + update_parse = "go/go(.*).linux-amd64.tar.gz" + version_parse = {'regex': 'go version go(.*) linux/amd64', 'command': 'go version'} def parse_download_link(self, line, in_download): """Parse Go download link, expect to find a sha and a url""" diff --git a/umake/frameworks/ide.py b/umake/frameworks/ide.py index 5d490f5f..5b0fc39f 100644 --- a/umake/frameworks/ide.py +++ b/umake/frameworks/ide.py @@ -760,7 +760,14 @@ def __init__(self, **kwargs): required_files_path=["atom", "resources/app/apm/bin/apm"], dir_to_decompress_in_tarball="atom-*", packages_requirements=["libgconf-2-4"], - checksum_type=ChecksumType.md5, **kwargs) + checksum_type=ChecksumType.md5, + updatable=True, **kwargs) + + def get_upstream_version(self, result): + page = result[self.download_page] + return json.loads(page.buffer.read().decode())["name"] + + version_parse = {'regex': 'Atom.*: (.*)\n', 'command': 'atom --version'} @MainLoop.in_mainloop_thread def get_metadata_and_check_license(self, result): @@ -882,6 +889,7 @@ def __init__(self, **kwargs): desktop_filename="sublime-text.desktop", required_files_path=["sublime_text"], dir_to_decompress_in_tarball="sublime_text_*", + updatable=True, **kwargs) arch_trans = { @@ -889,6 +897,9 @@ def __init__(self, **kwargs): "i386": "x32" } + update_parse = "https://download.sublimetext.com/sublime_text_3_build_(.*)_x64.tar.bz2" + version_parse = {'regex': 'Sublime Text Build (.*)\n', 'command': 'sublime-text --version'} + def parse_download_link(self, line, in_download): """Parse SublimeText download links""" url = None diff --git a/umake/frameworks/web.py b/umake/frameworks/web.py index d1d5d18e..e865ddbe 100644 --- a/umake/frameworks/web.py +++ b/umake/frameworks/web.py @@ -27,6 +27,7 @@ import os import platform import re +import subprocess import umake.frameworks.baseinstaller from umake.interactions import Choice, TextWithChoices, DisplayMessage from umake.network.download_center import DownloadItem @@ -56,6 +57,8 @@ def __init__(self, **kwargs): required_files_path=["firefox"], **kwargs) self.arg_lang = None + version_parse = {'regex': 'Mozilla Firefox (.*)\n', 'command': 'firefox-developer --version'} + @MainLoop.in_mainloop_thread def language_select_callback(self, url): url = url.replace("&", "&") diff --git a/umake/ui/cli/__init__.py b/umake/ui/cli/__init__.py index f28fa23d..b1613b8f 100644 --- a/umake/ui/cli/__init__.py +++ b/umake/ui/cli/__init__.py @@ -20,11 +20,13 @@ """Module for loading the command line interface""" import argcomplete +from argparse import Namespace from contextlib import suppress from gettext import gettext as _ import logging import os from progressbar import ProgressBar, BouncingBar +import re import readline import sys from umake.interactions import InputText, TextWithChoices, LicenseAgreement, DisplayMessage, UnknownProgress @@ -158,7 +160,7 @@ def mangle_args_for_default_framework(args): return result_args -def get_frameworks_list_output(args): +def get_frameworks_list_output(args, showPath=True, showDescription=True): """ Get a frameworks list based on the arguments. It returns a string ready to be printed. Multiple forms of the frameworks list can ge given: @@ -210,9 +212,12 @@ def get_frameworks_list_output(args): # Sort the frameworks to prevent a random list at each new program execution for framework in sorted(category["frameworks"], key=lambda fram: fram["framework_name"]): if framework["is_installed"]: - print_result += "{}: {}\n".format(framework["framework_name"], - framework["framework_description"]) - print_result += "\t{}: {}\n".format(_("path"), framework["install_path"]) + print_result += "{}".format(framework["framework_name"]) + if showDescription: + print_result += ": {}".format(framework["framework_description"]) + print_result += "\n" + if showPath: + print_result += "\t{}: {}\n".format(_("path"), framework["install_path"]) if not print_result: print_result = _("No frameworks are currently installed") @@ -239,7 +244,27 @@ def main(parser): print(get_frameworks_list_output(args)) sys.exit(0) - if args.version: + if args.update and not len(arg_to_parse) > 1: + args.list_installed=True + categories = list_frameworks() + installed = get_frameworks_list_output(args, showPath=False, showDescription=False) + for category in sorted(categories, key=lambda cat: cat["category_name"]): + # Sort the frameworks to prevent a random list at each new program execution + for framework in sorted(category["frameworks"], key=lambda fram: fram["framework_name"]): + if framework['framework_name'] in installed: + if framework['updatable']: + args = Namespace(category=category['category_name'], + destdir=framework['install_path'], + framework=framework['framework_name'], + update=True, version=False, beta=False, + remove=False) + print("Checking " + framework['framework_name']) + # print(args) + # args = parser.parse_args(args) + run_command_for_args(args) + # sys.exit(0) + + if args.version and not len(arg_to_parse) > 1: print(get_version()) sys.exit(0)