Пример #1
0
    def prepare(self, task_manager: TaskManager, root_password: Optional[str],
                internet_available: bool):
        create_config = CreateConfigFile(taskman=task_manager,
                                         configman=self.configman,
                                         i18n=self.i18n,
                                         task_icon_path=get_icon_path(),
                                         logger=self.logger)
        create_config.start()

        task_manager.register_task('snap_cats',
                                   self.i18n['task.download_categories'],
                                   get_icon_path())
        category_downloader = CategoriesDownloader(
            id_='snap',
            manager=self,
            http_client=self.http_client,
            logger=self.logger,
            url_categories_file=URL_CATEGORIES_FILE,
            categories_path=CATEGORIES_FILE_PATH,
            internet_connection=internet_available,
            internet_checker=self.context.internet_checker,
            after=lambda: self._finish_category_task(task_manager))
        category_downloader.before = lambda: self._start_category_task(
            task_manager, create_config, category_downloader)
        category_downloader.start()
Пример #2
0
class SnapManager(SoftwareManager):
    def __init__(self, context: ApplicationContext):
        super(SnapManager, self).__init__(context=context)
        self.i18n = context.i18n
        self.api_cache = context.cache_factory.new()
        context.disk_loader_factory.map(SnapApplication, self.api_cache)
        self.enabled = True
        self.http_client = context.http_client
        self.logger = context.logger
        self.ubuntu_distro = context.distro == 'ubuntu'
        self.categories = {}
        self.categories_downloader = CategoriesDownloader(
            'snap', self.http_client, self.logger, self, context.disk_cache,
            URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH)
        self.suggestions_cache = context.cache_factory.new()
        self.info_path = None

    def get_info_path(self) -> str:
        if self.info_path is None:
            self.info_path = snap.get_app_info_path()

        return self.info_path

    def map_json(self,
                 app_json: dict,
                 installed: bool,
                 disk_loader: DiskCacheLoader,
                 internet: bool = True) -> SnapApplication:
        app = SnapApplication(
            publisher=app_json.get('publisher'),
            rev=app_json.get('rev'),
            notes=app_json.get('notes'),
            has_apps_field=app_json.get('apps_field', False),
            id=app_json.get('name'),
            name=app_json.get('name'),
            version=app_json.get('version'),
            latest_version=app_json.get('version'),
            description=app_json.get('description', app_json.get('summary')),
            verified_publisher=app_json.get('developer_validation',
                                            '') == 'verified')

        if app.publisher and app.publisher.endswith('*'):
            app.verified_publisher = True
            app.publisher = app.publisher.replace('*', '')

        categories = self.categories.get(app.name.lower())

        if categories:
            app.categories = categories

        app.installed = installed

        if not app.is_application():
            categories = app.categories

            if categories is None:
                categories = []
                app.categories = categories

            if 'runtime' not in categories:
                categories.append('runtime')

        api_data = self.api_cache.get(app_json['name'])
        expired_data = api_data and api_data.get(
            'expires_at') and api_data['expires_at'] <= datetime.utcnow()

        if (not api_data or expired_data) and app.is_application():
            if disk_loader and app.installed:
                disk_loader.fill(app)

            if internet:
                SnapAsyncDataLoader(app=app,
                                    api_cache=self.api_cache,
                                    manager=self,
                                    context=self.context).start()
        else:
            app.fill_cached_data(api_data)

        return app

    def search(self,
               words: str,
               disk_loader: DiskCacheLoader,
               limit: int = -1,
               is_url: bool = False) -> SearchResult:
        if is_url:
            return SearchResult([], [], 0)

        if snap.is_snapd_running():
            installed = self.read_installed(disk_loader).installed

            res = SearchResult([], [], 0)

            for app_json in snap.search(words):

                already_installed = None

                if installed:
                    already_installed = [
                        i for i in installed if i.id == app_json.get('name')
                    ]
                    already_installed = already_installed[
                        0] if already_installed else None

                if already_installed:
                    res.installed.append(already_installed)
                else:
                    res.new.append(
                        self.map_json(app_json,
                                      installed=False,
                                      disk_loader=disk_loader))

            res.total = len(res.installed) + len(res.new)
            return res
        else:
            return SearchResult([], [], 0)

    def read_installed(self,
                       disk_loader: DiskCacheLoader,
                       limit: int = -1,
                       only_apps: bool = False,
                       pkg_types: Set[Type[SoftwarePackage]] = None,
                       internet_available: bool = None) -> SearchResult:
        info_path = self.get_info_path()

        if snap.is_snapd_running() and info_path:
            self.categories_downloader.join()
            installed = [
                self.map_json(app_json,
                              installed=True,
                              disk_loader=disk_loader,
                              internet=internet_available)
                for app_json in snap.read_installed(info_path)
            ]
            return SearchResult(installed, None, len(installed))
        else:
            return SearchResult([], None, 0)

    def downgrade(self, pkg: SnapApplication, root_password: str,
                  watcher: ProcessWatcher) -> bool:
        return ProcessHandler(watcher).handle(
            SystemProcess(subproc=snap.downgrade_and_stream(
                pkg.name, root_password),
                          wrong_error_phrase=None))

    def update(self, pkg: SnapApplication, root_password: str,
               watcher: ProcessWatcher) -> SystemProcess:
        raise Exception("'update' is not supported by {}".format(
            pkg.__class__.__name__))

    def uninstall(self, pkg: SnapApplication, root_password: str,
                  watcher: ProcessWatcher) -> bool:
        uninstalled = ProcessHandler(watcher).handle(
            SystemProcess(
                subproc=snap.uninstall_and_stream(pkg.name, root_password)))

        if self.suggestions_cache:
            self.suggestions_cache.delete(pkg.name)

        return uninstalled

    def get_managed_types(self) -> Set[Type[SoftwarePackage]]:
        return {SnapApplication}

    def clean_cache_for(self, pkg: SnapApplication):
        super(SnapManager, self).clean_cache_for(pkg)
        self.api_cache.delete(pkg.id)

    def get_info(self, pkg: SnapApplication) -> dict:
        info = snap.get_info(pkg.name,
                             attrs=('license', 'contact', 'commands',
                                    'snap-id', 'tracking', 'installed'))
        info['description'] = pkg.description
        info['publisher'] = pkg.publisher
        info['revision'] = pkg.rev
        info['name'] = pkg.name

        if info.get('commands'):
            info['commands'] = ' '.join(info['commands'])

        if info.get('license') and info['license'] == 'unset':
            del info['license']

        return info

    def get_history(self, pkg: SnapApplication) -> PackageHistory:
        raise Exception("'get_history' is not supported by {}".format(
            pkg.__class__.__name__))

    def install(self, pkg: SnapApplication, root_password: str,
                watcher: ProcessWatcher) -> bool:
        info_path = self.get_info_path()

        if not info_path:
            self.logger.warning(
                'Information directory was not found. It will not be possible to determine if the installed application can be launched'
            )

        res, output = ProcessHandler(watcher).handle_simple(
            snap.install_and_stream(pkg.name, pkg.confinement, root_password))

        if 'error:' in output:
            res = False
            if 'not available on stable' in output:
                channels = RE_AVAILABLE_CHANNELS.findall(output)

                if channels:
                    opts = [
                        InputOption(label=c[0], value=c[1]) for c in channels
                    ]
                    channel_select = SingleSelectComponent(
                        type_=SelectViewType.RADIO,
                        label='',
                        options=opts,
                        default_option=opts[0])
                    body = '<p>{}.</p>'.format(
                        self.i18n['snap.install.available_channels.message'].
                        format(bold(self.i18n['stable']), bold(pkg.name)))
                    body += '<p>{}:</p>'.format(
                        self.i18n['snap.install.available_channels.help'])

                    if watcher.request_confirmation(
                            title=self.
                            i18n['snap.install.available_channels.title'],
                            body=body,
                            components=[channel_select],
                            confirmation_label=self.i18n['continue'],
                            deny_label=self.i18n['cancel']):
                        self.logger.info(
                            "Installing '{}' with the custom command '{}'".
                            format(pkg.name, channel_select.value))
                        res = ProcessHandler(watcher).handle(
                            SystemProcess(
                                new_root_subprocess(
                                    channel_select.value.value.split(' '),
                                    root_password=root_password)))

                        if res and info_path:
                            pkg.has_apps_field = snap.has_apps_field(
                                pkg.name, info_path)

                        return res
                else:
                    self.logger.error(
                        "Could not find available channels in the installation output: {}"
                        .format(output))
        else:
            if info_path:
                pkg.has_apps_field = snap.has_apps_field(pkg.name, info_path)

        return res

    def is_enabled(self) -> bool:
        return self.enabled

    def set_enabled(self, enabled: bool):
        self.enabled = enabled

    def can_work(self) -> bool:
        return snap.is_installed()

    def requires_root(self, action: str, pkg: SnapApplication):
        return action != 'search'

    def refresh(self, pkg: SnapApplication, root_password: str,
                watcher: ProcessWatcher) -> bool:
        return ProcessHandler(watcher).handle(
            SystemProcess(
                subproc=snap.refresh_and_stream(pkg.name, root_password)))

    def prepare(self):
        self.categories_downloader.start()

    def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
        pass

    def list_warnings(self, internet_available: bool) -> List[str]:
        if snap.is_installed():
            if not snap.is_snapd_running():
                snap_bold = bold('Snap')
                return [
                    self.i18n['snap.notification.snapd_unavailable'].format(
                        bold('snapd'), snap_bold),
                    self.i18n['snap.notification.snap.disable'].format(
                        snap_bold,
                        bold('{} > {}'.format(
                            self.i18n['settings'].capitalize(),
                            self.i18n['core.config.tab.types'])))
                ]

            elif internet_available:
                available, output = snap.is_api_available()

                if not available:
                    self.logger.warning(
                        'It seems Snap API is not available. Search output: {}'
                        .format(output))
                    return [
                        self.i18n['snap.notifications.api.unavailable'].format(
                            bold('Snaps'), bold('Snap'))
                    ]

    def _fill_suggestion(self, pkg_name: str, priority: SuggestionPriority,
                         out: List[PackageSuggestion]):
        res = self.http_client.get_json(
            SNAP_API_URL + '/search?q=package_name:{}'.format(pkg_name))

        if res and res['_embedded']['clickindex:package']:
            pkg = res['_embedded']['clickindex:package'][0]
            pkg['rev'] = pkg['revision']
            pkg['name'] = pkg_name

            sug = PackageSuggestion(
                self.map_json(pkg, installed=False, disk_loader=None),
                priority)
            self.suggestions_cache.add(pkg_name, sug)
            out.append(sug)
        else:
            self.logger.warning(
                "Could not retrieve suggestion '{}'".format(pkg_name))

    def list_suggestions(self, limit: int,
                         filter_installed: bool) -> List[PackageSuggestion]:
        res = []

        if snap.is_snapd_running():
            self.logger.info(
                'Downloading suggestions file {}'.format(SUGGESTIONS_FILE))
            file = self.http_client.get(SUGGESTIONS_FILE)

            if not file or not file.text:
                self.logger.warning(
                    "No suggestion found in {}".format(SUGGESTIONS_FILE))
                return res
            else:
                self.logger.info('Mapping suggestions')
                self.categories_downloader.join()

                suggestions, threads = [], []
                installed = {
                    i.name.lower()
                    for i in self.read_installed(disk_loader=None).installed
                } if filter_installed else None

                for l in file.text.split('\n'):
                    if l:
                        if limit <= 0 or len(suggestions) < limit:
                            sug = l.strip().split('=')
                            name = sug[1]

                            if not installed or name not in installed:
                                cached_sug = self.suggestions_cache.get(name)

                                if cached_sug:
                                    res.append(cached_sug)
                                else:
                                    t = Thread(target=self._fill_suggestion,
                                               args=(name,
                                                     SuggestionPriority(
                                                         int(sug[0])), res))
                                    t.start()
                                    threads.append(t)
                                    time.sleep(0.001)  # to avoid being blocked
                        else:
                            break

                for t in threads:
                    t.join()

                res.sort(key=lambda s: s.priority.value, reverse=True)
        return res

    def is_default_enabled(self) -> bool:
        return True

    def launch(self, pkg: SnapApplication):
        snap.run(pkg, self.context.logger)

    def get_screenshots(self, pkg: SoftwarePackage) -> List[str]:
        res = self.http_client.get_json('{}/search?q={}'.format(
            SNAP_API_URL, pkg.name))

        if res:
            if res.get('_embedded') and res['_embedded'].get(
                    'clickindex:package'):
                snap_data = res['_embedded']['clickindex:package'][0]

                if snap_data.get('screenshot_urls'):
                    return snap_data['screenshot_urls']
                else:
                    self.logger.warning(
                        "No 'screenshots_urls' defined for {}".format(pkg))
            else:
                self.logger.error(
                    'It seems the API is returning a different response: {}'.
                    format(res))
        else:
            self.logger.warning('Could not retrieve data for {}'.format(pkg))

        return []
Пример #3
0
class ArchManager(SoftwareManager):
    def __init__(self, context: ApplicationContext):
        super(ArchManager, self).__init__(context=context)
        self.aur_cache = context.cache_factory.new()
        # context.disk_loader_factory.map(ArchPackage, self.aur_cache) TODO

        self.mapper = ArchDataMapper(http_client=context.http_client)
        self.i18n = context.i18n
        self.aur_client = AURClient(context.http_client)
        self.names_index = {}
        self.aur_index_updater = AURIndexUpdater(context, self)
        self.dcache_updater = ArchDiskCacheUpdater(context.logger,
                                                   context.disk_cache)
        self.comp_optimizer = ArchCompilationOptimizer(context.logger)
        self.logger = context.logger
        self.enabled = True
        self.arch_distro = context.distro == 'arch'
        self.categories_mapper = CategoriesDownloader(
            'AUR', context.http_client, context.logger, self,
            self.context.disk_cache, URL_CATEGORIES_FILE, CATEGORIES_CACHE_DIR,
            CATEGORIES_FILE_PATH)
        self.categories = {}

    def _upgrade_search_result(self, apidata: dict, installed_pkgs: dict,
                               downgrade_enabled: bool, res: SearchResult,
                               disk_loader: DiskCacheLoader):
        app = self.mapper.map_api_data(apidata, installed_pkgs['not_signed'],
                                       self.categories)
        app.downgrade_enabled = downgrade_enabled

        if app.installed:
            res.installed.append(app)

            if disk_loader:
                disk_loader.fill(app)
        else:
            res.new.append(app)

        Thread(target=self.mapper.fill_package_build,
               args=(app, ),
               daemon=True).start()

    def search(self,
               words: str,
               disk_loader: DiskCacheLoader,
               limit: int = -1) -> SearchResult:
        self.comp_optimizer.join()

        downgrade_enabled = git.is_enabled()
        res = SearchResult([], [], 0)

        installed = {}
        read_installed = Thread(
            target=lambda: installed.update(pacman.list_and_map_installed()),
            daemon=True)
        read_installed.start()

        mapped_words = SEARCH_OPTIMIZED_MAP.get(words)

        api_res = self.aur_client.search(
            mapped_words if mapped_words else words)

        if api_res and api_res.get('results'):
            read_installed.join()

            for pkgdata in api_res['results']:
                self._upgrade_search_result(pkgdata, installed,
                                            downgrade_enabled, res,
                                            disk_loader)

        else:  # if there are no results from the API (it could be because there were too many), tries the names index:
            if self.names_index:

                to_query = set()
                for norm_name, real_name in self.names_index.items():
                    if words in norm_name:
                        to_query.add(real_name)

                    if len(to_query) == 25:
                        break

                pkgsinfo = self.aur_client.get_info(to_query)

                if pkgsinfo:
                    read_installed.join()

                    for pkgdata in pkgsinfo:
                        self._upgrade_search_result(pkgdata, installed, res)

        res.total = len(res.installed) + len(res.new)
        return res

    def _fill_aur_pkgs(self, not_signed: dict, pkgs: list,
                       disk_loader: DiskCacheLoader, internet_available: bool):
        downgrade_enabled = git.is_enabled()

        if internet_available:
            try:
                pkgsinfo = self.aur_client.get_info(not_signed.keys())

                if pkgsinfo:
                    for pkgdata in pkgsinfo:
                        pkg = self.mapper.map_api_data(pkgdata, not_signed,
                                                       self.categories)
                        pkg.downgrade_enabled = downgrade_enabled

                        if disk_loader:
                            disk_loader.fill(pkg)
                            pkg.status = PackageStatus.READY

                        pkgs.append(pkg)

                return
            except requests.exceptions.ConnectionError:
                self.logger.warning(
                    'Could not retrieve installed AUR packages API data. It seems the internet connection is off.'
                )
                self.logger.info("Reading only local AUR packages data")

        for name, data in not_signed.items():
            pkg = ArchPackage(name=name,
                              version=data.get('version'),
                              latest_version=data.get('version'),
                              description=data.get('description'),
                              installed=True,
                              mirror='aur')

            pkg.categories = self.categories.get(pkg.name)
            pkg.downgrade_enabled = downgrade_enabled

            if disk_loader:
                disk_loader.fill(pkg)
                pkg.status = PackageStatus.READY

            pkgs.append(pkg)

    def _fill_mirror_pkgs(self, mirrors: dict, apps: list):
        # TODO
        for name, data in mirrors.items():
            app = ArchPackage(name=name,
                              version=data.get('version'),
                              latest_version=data.get('version'),
                              description=data.get('description'))
            app.installed = True
            app.mirror = ''  # TODO
            app.update = False  # TODO
            apps.append(app)

    def read_installed(self,
                       disk_loader: DiskCacheLoader,
                       limit: int = -1,
                       only_apps: bool = False,
                       pkg_types: Set[Type[SoftwarePackage]] = None,
                       internet_available: bool = None) -> SearchResult:
        installed = pacman.list_and_map_installed()

        apps = []
        if installed and installed['not_signed']:
            self.dcache_updater.join()
            self.categories_mapper.join()

            self._fill_aur_pkgs(installed['not_signed'], apps, disk_loader,
                                internet_available)

        return SearchResult(apps, None, len(apps))

    def downgrade(self, pkg: ArchPackage, root_password: str,
                  watcher: ProcessWatcher) -> bool:

        handler = ProcessHandler(watcher)
        app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))
        watcher.change_progress(5)

        try:
            if not os.path.exists(app_build_dir):
                build_dir = handler.handle(
                    SystemProcess(
                        new_subprocess(['mkdir', '-p', app_build_dir])))

                if build_dir:
                    watcher.change_progress(10)
                    watcher.change_substatus(self.i18n['arch.clone'].format(
                        bold(pkg.name)))
                    clone = handler.handle(
                        SystemProcess(subproc=new_subprocess(
                            ['git', 'clone',
                             URL_GIT.format(pkg.name)],
                            cwd=app_build_dir),
                                      check_error_output=False))
                    watcher.change_progress(30)
                    if clone:
                        watcher.change_substatus(
                            self.i18n['arch.downgrade.reading_commits'])
                        clone_path = '{}/{}'.format(app_build_dir, pkg.name)
                        pkgbuild_path = '{}/PKGBUILD'.format(clone_path)

                        commits = run_cmd("git log", cwd=clone_path)
                        watcher.change_progress(40)

                        if commits:
                            commit_list = re.findall(r'commit (.+)\n', commits)
                            if commit_list:
                                if len(commit_list) > 1:
                                    for idx in range(1, len(commit_list)):
                                        commit = commit_list[idx]
                                        with open(pkgbuild_path) as f:
                                            pkgdict = aur.map_pkgbuild(
                                                f.read())

                                        if not handler.handle(
                                                SystemProcess(
                                                    subproc=new_subprocess(
                                                        [
                                                            'git', 'reset',
                                                            '--hard', commit
                                                        ],
                                                        cwd=clone_path),
                                                    check_error_output=False)):
                                            watcher.print(
                                                'Could not downgrade anymore. Aborting...'
                                            )
                                            return False

                                        if '{}-{}'.format(
                                                pkgdict.get('pkgver'),
                                                pkgdict.get(
                                                    'pkgrel')) == pkg.version:
                                            # current version found
                                            watcher.change_substatus(self.i18n[
                                                'arch.downgrade.version_found']
                                                                     )
                                            break

                                    watcher.change_substatus(
                                        self.
                                        i18n['arch.downgrade.install_older'])
                                    return self._make_pkg(pkg.name,
                                                          pkg.maintainer,
                                                          root_password,
                                                          handler,
                                                          app_build_dir,
                                                          clone_path,
                                                          dependency=False,
                                                          skip_optdeps=True)
                                else:
                                    watcher.show_message(
                                        title=self.
                                        i18n['arch.downgrade.error'],
                                        body=self.
                                        i18n['arch.downgrade.impossible'].
                                        format(pkg.name),
                                        type_=MessageType.ERROR)
                                    return False

                        watcher.show_message(
                            title=self.i18n['error'],
                            body=self.i18n['arch.downgrade.no_commits'],
                            type_=MessageType.ERROR)
                        return False

        finally:
            if os.path.exists(app_build_dir):
                handler.handle(
                    SystemProcess(
                        subproc=new_subprocess(['rm', '-rf', app_build_dir])))

        return False

    def clean_cache_for(self, pkg: ArchPackage):
        if os.path.exists(pkg.get_disk_cache_path()):
            shutil.rmtree(pkg.get_disk_cache_path())

    def update(self, pkg: ArchPackage, root_password: str,
               watcher: ProcessWatcher) -> bool:
        return self.install(pkg=pkg,
                            root_password=root_password,
                            watcher=watcher,
                            skip_optdeps=True)

    def _uninstall(self, pkg_name: str, root_password: str,
                   handler: ProcessHandler) -> bool:
        res = handler.handle(
            SystemProcess(
                new_root_subprocess(['pacman', '-R', pkg_name, '--noconfirm'],
                                    root_password)))

        if res:
            cached_paths = [
                ArchPackage.disk_cache_path(pkg_name, 'aur'),
                ArchPackage.disk_cache_path(pkg_name, 'mirror')
            ]

            for path in cached_paths:
                if os.path.exists(path):
                    shutil.rmtree(path)
                    break

        return res

    def uninstall(self, pkg: ArchPackage, root_password: str,
                  watcher: ProcessWatcher) -> bool:
        handler = ProcessHandler(watcher)

        watcher.change_progress(10)
        info = pacman.get_info_dict(pkg.name)
        watcher.change_progress(50)

        if info.get('required by'):
            pkname = bold(pkg.name)
            msg = '{}:<br/><br/>{}<br/><br/>{}'.format(
                self.i18n['arch.uninstall.required_by'].format(pkname),
                bold(info['required by']),
                self.i18n['arch.uninstall.required_by.advice'].format(pkname))
            watcher.show_message(title=self.i18n['error'],
                                 body=msg,
                                 type_=MessageType.WARNING)
            return False

        uninstalled = self._uninstall(pkg.name, root_password, handler)
        watcher.change_progress(100)
        return uninstalled

    def get_managed_types(self) -> Set["type"]:
        return {ArchPackage}

    def get_info(self, pkg: ArchPackage) -> dict:
        if pkg.installed:
            t = Thread(target=self.mapper.fill_package_build, args=(pkg, ))
            t.start()

            info = pacman.get_info_dict(pkg.name)

            t.join()

            if pkg.pkgbuild:
                info['13_pkg_build'] = pkg.pkgbuild

            info['14_installed_files'] = pacman.list_installed_files(pkg.name)

            return info
        else:
            info = {
                '01_id': pkg.id,
                '02_name': pkg.name,
                '03_version': pkg.version,
                '04_popularity': pkg.popularity,
                '05_votes': pkg.votes,
                '06_package_base': pkg.package_base,
                '07_maintainer': pkg.maintainer,
                '08_first_submitted': pkg.first_submitted,
                '09_last_modified': pkg.last_modified,
                '10_url': pkg.url_download
            }

            srcinfo = self.aur_client.get_src_info(pkg.name)

            if srcinfo:
                if srcinfo.get('depends'):
                    info['11_dependson'] = srcinfo['depends']

                if srcinfo.get('optdepends'):
                    info['12_optdepends'] = srcinfo['optdepends']

            if pkg.pkgbuild:
                info['00_pkg_build'] = pkg.pkgbuild
            else:
                info['11_pkg_build_url'] = pkg.get_pkg_build_url()

            return info

    def get_history(self, pkg: ArchPackage) -> PackageHistory:
        temp_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))

        try:
            Path(temp_dir).mkdir(parents=True)
            run_cmd('git clone ' + URL_GIT.format(pkg.name),
                    print_error=False,
                    cwd=temp_dir)

            clone_path = '{}/{}'.format(temp_dir, pkg.name)
            pkgbuild_path = '{}/PKGBUILD'.format(clone_path)

            commits = git.list_commits(clone_path)

            if commits:
                history, status_idx = [], -1

                for idx, commit in enumerate(commits):
                    with open(pkgbuild_path) as f:
                        pkgdict = aur.map_pkgbuild(f.read())

                    if status_idx < 0 and '{}-{}'.format(
                            pkgdict.get('pkgver'),
                            pkgdict.get('pkgrel')) == pkg.version:
                        status_idx = idx

                    history.append({
                        '1_version': pkgdict['pkgver'],
                        '2_release': pkgdict['pkgrel'],
                        '3_date': commit['date']
                    })  # the number prefix is to ensure the rendering order

                    if idx + 1 < len(commits):
                        if not run_cmd('git reset --hard ' +
                                       commits[idx + 1]['commit'],
                                       cwd=clone_path):
                            break

                return PackageHistory(pkg=pkg,
                                      history=history,
                                      pkg_status_idx=status_idx)
        finally:
            if os.path.exists(temp_dir):
                shutil.rmtree(temp_dir)

    def _install_deps(self,
                      deps: Set[str],
                      pkg_mirrors: dict,
                      root_password: str,
                      handler: ProcessHandler,
                      change_progress: bool = False) -> str:
        """
        :param deps:
        :param pkg_mirrors:
        :param root_password:
        :param handler:
        :return: not installed dependency
        """
        progress_increment = int(100 / len(deps))
        progress = 0
        self._update_progress(handler.watcher, 1, change_progress)

        for pkgname in deps:

            mirror = pkg_mirrors[pkgname]
            handler.watcher.change_substatus(
                self.i18n['arch.install.dependency.install'].format(
                    bold('{} ()'.format(pkgname, mirror))))
            if mirror == 'aur':
                installed = self._install_from_aur(pkgname=pkgname,
                                                   maintainer=None,
                                                   root_password=root_password,
                                                   handler=handler,
                                                   dependency=True,
                                                   change_progress=False)
            else:
                installed = self._install(pkgname=pkgname,
                                          maintainer=None,
                                          root_password=root_password,
                                          handler=handler,
                                          install_file=None,
                                          mirror=mirror,
                                          change_progress=False)

            if not installed:
                return pkgname

            progress += progress_increment
            self._update_progress(handler.watcher, progress, change_progress)

        self._update_progress(handler.watcher, 100, change_progress)

    def _map_mirrors(self, pkgnames: Set[str]) -> dict:
        pkg_mirrors = pacman.get_mirrors(pkgnames)  # getting mirrors set

        if len(
                pkgnames
        ) != pkg_mirrors:  # checking if any dep not found in the distro mirrors are from AUR
            nomirrors = {p for p in pkgnames if p not in pkg_mirrors}
            for pkginfo in self.aur_client.get_info(nomirrors):
                if pkginfo.get('Name') in nomirrors:
                    pkg_mirrors[pkginfo['Name']] = 'aur'

        return pkg_mirrors

    def _pre_download_source(self, pkgname: str, project_dir: str,
                             watcher: ProcessWatcher) -> bool:
        if self.context.file_downloader.is_multithreaded():
            srcinfo = self.aur_client.get_src_info(pkgname)

            pre_download_files = []

            for attr in SOURCE_FIELDS:
                if srcinfo.get(attr):
                    if attr == 'source_x86_x64' and not self.context.is_system_x86_64(
                    ):
                        continue
                    else:
                        for f in srcinfo[attr]:
                            if RE_PRE_DOWNLOADABLE_FILES.findall(f):
                                pre_download_files.append(f)

            if pre_download_files:
                for f in pre_download_files:
                    fdata = f.split('::')

                    args = {'watcher': watcher, 'cwd': project_dir}
                    if len(fdata) > 1:
                        args.update({
                            'file_url': fdata[1],
                            'output_path': fdata[0]
                        })
                    else:
                        args.update({
                            'file_url': fdata[0],
                            'output_path': None
                        })

                    if not self.context.file_downloader.download(**args):
                        watcher.print(
                            'Could not download source file {}'.format(
                                args['file_url']))
                        return False

        return True

    def _make_pkg(self,
                  pkgname: str,
                  maintainer: str,
                  root_password: str,
                  handler: ProcessHandler,
                  build_dir: str,
                  project_dir: str,
                  dependency: bool,
                  skip_optdeps: bool = False,
                  change_progress: bool = True) -> bool:

        self._pre_download_source(pkgname, project_dir, handler.watcher)

        self._update_progress(handler.watcher, 50, change_progress)
        if not self._install_missings_deps_and_keys(pkgname, root_password,
                                                    handler, project_dir):
            return False

        # building main package
        handler.watcher.change_substatus(
            self.i18n['arch.building.package'].format(bold(pkgname)))
        pkgbuilt, output = handler.handle_simple(
            SimpleProcess(['makepkg', '-ALcsmf'], cwd=project_dir))
        self._update_progress(handler.watcher, 65, change_progress)

        if pkgbuilt:
            gen_file = [
                fname for root, dirs, files in os.walk(build_dir)
                for fname in files
                if re.match(r'^{}-.+\.tar\.xz'.format(pkgname), fname)
            ]

            if not gen_file:
                handler.watcher.print(
                    'Could not find generated .tar.xz file. Aborting...')
                return False

            install_file = '{}/{}'.format(project_dir, gen_file[0])

            if self._install(pkgname=pkgname,
                             maintainer=maintainer,
                             root_password=root_password,
                             mirror='aur',
                             handler=handler,
                             install_file=install_file,
                             pkgdir=project_dir,
                             change_progress=change_progress):

                if dependency or skip_optdeps:
                    return True

                handler.watcher.change_substatus(
                    self.i18n['arch.optdeps.checking'].format(bold(pkgname)))

                if self._install_optdeps(pkgname,
                                         root_password,
                                         handler,
                                         project_dir,
                                         change_progress=change_progress):
                    return True

        return False

    def _install_missings_deps_and_keys(self, pkgname: str, root_password: str,
                                        handler: ProcessHandler,
                                        pkgdir: str) -> bool:
        handler.watcher.change_substatus(
            self.i18n['arch.checking.deps'].format(bold(pkgname)))
        check_res = makepkg.check(pkgdir, handler)

        if check_res:
            if check_res.get('missing_deps'):
                depnames = {
                    RE_SPLIT_VERSION.split(dep)[0]
                    for dep in check_res['missing_deps']
                }
                dep_mirrors = self._map_mirrors(depnames)

                for dep in depnames:  # cheking if a dependency could not be found in any mirror
                    if dep not in dep_mirrors:
                        message.show_dep_not_found(dep, self.i18n,
                                                   handler.watcher)
                        return False

                handler.watcher.change_substatus(
                    self.i18n['arch.missing_deps_found'].format(bold(pkgname)))

                if not confirmation.request_install_missing_deps(
                        pkgname, dep_mirrors, handler.watcher, self.i18n):
                    handler.watcher.print(self.i18n['action.cancelled'])
                    return False

                dep_not_installed = self._install_deps(depnames,
                                                       dep_mirrors,
                                                       root_password,
                                                       handler,
                                                       change_progress=False)

                if dep_not_installed:
                    message.show_dep_not_installed(handler.watcher, pkgname,
                                                   dep_not_installed,
                                                   self.i18n)
                    return False

                # it is necessary to re-check because missing PGP keys are only notified when there are none missing
                return self._install_missings_deps_and_keys(
                    pkgname, root_password, handler, pkgdir)

            if check_res.get('gpg_key'):
                if handler.watcher.request_confirmation(
                        title=self.i18n['arch.aur.install.unknown_key.title'],
                        body=self.i18n['arch.install.aur.unknown_key.body'].
                        format(bold(pkgname), bold(check_res['gpg_key']))):
                    handler.watcher.change_substatus(
                        self.i18n['arch.aur.install.unknown_key.status'].
                        format(bold(check_res['gpg_key'])))
                    if not handler.handle(gpg.receive_key(
                            check_res['gpg_key'])):
                        handler.watcher.show_message(
                            title=self.i18n['error'],
                            body=self.
                            i18n['arch.aur.install.unknown_key.receive_error'].
                            format(bold(check_res['gpg_key'])))
                        return False
                else:
                    handler.watcher.print(self.i18n['action.cancelled'])
                    return False

            if check_res.get('validity_check'):
                handler.watcher.show_message(
                    title=self.i18n['arch.aur.install.validity_check.title'],
                    body=self.i18n['arch.aur.install.validity_check.body'].
                    format(bold(pkgname)),
                    type_=MessageType.ERROR)
                return False

        return True

    def _install_optdeps(self,
                         pkgname: str,
                         root_password: str,
                         handler: ProcessHandler,
                         pkgdir: str,
                         change_progress: bool = True) -> bool:
        with open('{}/.SRCINFO'.format(pkgdir)) as f:
            odeps = pkgbuild.read_optdeps_as_dict(f.read())

        if not odeps:
            return True

        to_install = {d for d in odeps if not pacman.check_installed(d)}

        if not to_install:
            return True

        pkg_mirrors = self._map_mirrors(to_install)

        if pkg_mirrors:
            final_optdeps = {
                dep: {
                    'desc': odeps.get(dep),
                    'mirror': pkg_mirrors.get(dep)
                }
                for dep in to_install if dep in pkg_mirrors
            }

            deps_to_install = confirmation.request_optional_deps(
                pkgname, final_optdeps, handler.watcher, self.i18n)

            if not deps_to_install:
                return True
            else:
                dep_not_installed = self._install_deps(deps_to_install,
                                                       pkg_mirrors,
                                                       root_password,
                                                       handler,
                                                       change_progress=True)

                if dep_not_installed:
                    message.show_optdep_not_installed(dep_not_installed,
                                                      handler.watcher,
                                                      self.i18n)
                    return False

        return True

    def _install(self,
                 pkgname: str,
                 maintainer: str,
                 root_password: str,
                 mirror: str,
                 handler: ProcessHandler,
                 install_file: str = None,
                 pkgdir: str = '.',
                 change_progress: bool = True):
        check_install_output = []
        pkgpath = install_file if install_file else pkgname

        handler.watcher.change_substatus(
            self.i18n['arch.checking.conflicts'].format(bold(pkgname)))

        for check_out in SimpleProcess(
            ['pacman', '-U' if install_file else '-S', pkgpath],
                root_password=root_password,
                cwd=pkgdir).instance.stdout:
            check_install_output.append(check_out.decode())

        self._update_progress(handler.watcher, 70, change_progress)
        if check_install_output and 'conflict' in check_install_output[-1]:
            conflicting_apps = [
                w[0] for w in re.findall(r'((\w|\-|\.)+)\s(and|are)',
                                         check_install_output[-1])
            ]
            conflict_msg = ' {} '.format(self.i18n['and']).join(
                [bold(c) for c in conflicting_apps])
            if not handler.watcher.request_confirmation(
                    title=self.i18n['arch.install.conflict.popup.title'],
                    body=self.i18n['arch.install.conflict.popup.body'].format(
                        conflict_msg)):
                handler.watcher.print(self.i18n['action.cancelled'])
                return False
            else:  # uninstall conflicts
                self._update_progress(handler.watcher, 75, change_progress)
                to_uninstall = [
                    conflict for conflict in conflicting_apps
                    if conflict != pkgname
                ]

                for conflict in to_uninstall:
                    handler.watcher.change_substatus(
                        self.i18n['arch.uninstalling.conflict'].format(
                            bold(conflict)))
                    if not self._uninstall(conflict, root_password, handler):
                        handler.watcher.show_message(
                            title=self.i18n['error'],
                            body=self.i18n['arch.uninstalling.conflict.fail'].
                            format(bold(conflict)),
                            type_=MessageType.ERROR)
                        return False

        handler.watcher.change_substatus(
            self.i18n['arch.installing.package'].format(bold(pkgname)))
        self._update_progress(handler.watcher, 80, change_progress)
        installed = handler.handle(
            pacman.install_as_process(pkgpath=pkgpath,
                                      root_password=root_password,
                                      aur=install_file is not None,
                                      pkgdir=pkgdir))
        self._update_progress(handler.watcher, 95, change_progress)

        if installed and self.context.disk_cache:
            handler.watcher.change_substatus(
                self.i18n['status.caching_data'].format(bold(pkgname)))
            if self.context.disk_cache:
                disk.save_several({pkgname},
                                  mirror=mirror,
                                  maintainer=maintainer,
                                  overwrite=True,
                                  categories=self.categories)

            self._update_progress(handler.watcher, 100, change_progress)

        return installed

    def _update_progress(self, watcher: ProcessWatcher, val: int,
                         change_progress: bool):
        if change_progress:
            watcher.change_progress(val)

    def _import_pgp_keys(self, pkgname: str, root_password: str,
                         handler: ProcessHandler):
        srcinfo = self.aur_client.get_src_info(pkgname)

        if srcinfo.get('validpgpkeys'):
            handler.watcher.print(self.i18n['arch.aur.install.verifying_pgp'])
            keys_to_download = [
                key for key in srcinfo['validpgpkeys']
                if not pacman.verify_pgp_key(key)
            ]

            if keys_to_download:
                keys_str = ''.join([
                    '<br/><span style="font-weight:bold">  - {}</span>'.format(
                        k) for k in keys_to_download
                ])
                msg_body = '{}:<br/>{}<br/><br/>{}'.format(
                    self.i18n['arch.aur.install.pgp.body'].format(
                        bold(pkgname)), keys_str, self.i18n['ask.continue'])

                if handler.watcher.request_confirmation(
                        title=self.i18n['arch.aur.install.pgp.title'],
                        body=msg_body):
                    for key in keys_to_download:
                        handler.watcher.change_substatus(
                            self.i18n['arch.aur.install.pgp.substatus'].format(
                                bold(key)))
                        if not handler.handle(
                                pacman.receive_key(key, root_password)):
                            handler.watcher.show_message(
                                title=self.i18n['error'],
                                body=self.
                                i18n['arch.aur.install.pgp.receive_fail'].
                                format(bold(key)),
                                type_=MessageType.ERROR)
                            return False

                        if not handler.handle(
                                pacman.sign_key(key, root_password)):
                            handler.watcher.show_message(
                                title=self.i18n['error'],
                                body=self.
                                i18n['arch.aur.install.pgp.sign_fail'].format(
                                    bold(key)),
                                type_=MessageType.ERROR)
                            return False

                        handler.watcher.change_substatus(
                            self.i18n['arch.aur.install.pgp.success'])
                else:
                    handler.watcher.print(self.i18n['action.cancelled'])
                    return False

    def _install_from_aur(self,
                          pkgname: str,
                          maintainer: str,
                          root_password: str,
                          handler: ProcessHandler,
                          dependency: bool,
                          skip_optdeps: bool = False,
                          change_progress: bool = True) -> bool:
        app_build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time()))

        try:
            if not os.path.exists(app_build_dir):
                build_dir = handler.handle(
                    SystemProcess(
                        new_subprocess(['mkdir', '-p', app_build_dir])))
                self._update_progress(handler.watcher, 10, change_progress)

                if build_dir:
                    file_url = URL_PKG_DOWNLOAD.format(pkgname)
                    file_name = file_url.split('/')[-1]
                    handler.watcher.change_substatus('{} {}'.format(
                        self.i18n['arch.downloading.package'],
                        bold(file_name)))
                    download = handler.handle(
                        SystemProcess(new_subprocess(['wget', file_url],
                                                     cwd=app_build_dir),
                                      check_error_output=False))

                    if download:
                        self._update_progress(handler.watcher, 30,
                                              change_progress)
                        handler.watcher.change_substatus('{} {}'.format(
                            self.i18n['arch.uncompressing.package'],
                            bold(file_name)))
                        uncompress = handler.handle(
                            SystemProcess(
                                new_subprocess([
                                    'tar', 'xvzf', '{}.tar.gz'.format(pkgname)
                                ],
                                               cwd=app_build_dir)))
                        self._update_progress(handler.watcher, 40,
                                              change_progress)

                        if uncompress:
                            uncompress_dir = '{}/{}'.format(
                                app_build_dir, pkgname)
                            return self._make_pkg(
                                pkgname=pkgname,
                                maintainer=maintainer,
                                root_password=root_password,
                                handler=handler,
                                build_dir=app_build_dir,
                                project_dir=uncompress_dir,
                                dependency=dependency,
                                skip_optdeps=skip_optdeps,
                                change_progress=change_progress)
        finally:
            if os.path.exists(app_build_dir):
                handler.handle(
                    SystemProcess(new_subprocess(['rm', '-rf',
                                                  app_build_dir])))

        return False

    def install(self,
                pkg: ArchPackage,
                root_password: str,
                watcher: ProcessWatcher,
                skip_optdeps: bool = False) -> bool:
        res = self._install_from_aur(pkg.name,
                                     pkg.maintainer,
                                     root_password,
                                     ProcessHandler(watcher),
                                     dependency=False,
                                     skip_optdeps=skip_optdeps)

        if res:
            if os.path.exists(pkg.get_disk_data_path()):
                with open(pkg.get_disk_data_path()) as f:
                    data = f.read()
                    if data:
                        data = json.loads(data)
                        pkg.fill_cached_data(data)

        return res

    def _is_wget_available(self):
        res = run_cmd('which wget')
        return res and not res.strip().startswith('which ')

    def is_enabled(self) -> bool:
        return self.enabled

    def set_enabled(self, enabled: bool):
        self.enabled = enabled

    def can_work(self) -> bool:
        try:
            return self.arch_distro and pacman.is_enabled(
            ) and self._is_wget_available()
        except FileNotFoundError:
            return False

    def is_downgrade_enabled(self) -> bool:
        try:
            new_subprocess(['git', '--version'])
            return True
        except FileNotFoundError:
            return False

    def cache_to_disk(self, pkg: ArchPackage, icon_bytes: bytes,
                      only_icon: bool):
        pass

    def requires_root(self, action: str, pkg: ArchPackage):
        return action != 'search'

    def prepare(self):
        self.dcache_updater.start()
        self.comp_optimizer.start()
        self.aur_index_updater.start()
        self.categories_mapper.start()

    def list_updates(self, internet_available: bool) -> List[PackageUpdate]:
        installed = self.read_installed(
            disk_loader=None, internet_available=internet_available).installed
        return [
            PackageUpdate(p.id, p.latest_version, 'aur') for p in installed
            if p.update
        ]

    def list_warnings(self, internet_available: bool) -> List[str]:
        warnings = []

        if self.arch_distro:
            if not pacman.is_enabled():
                warnings.append(self.i18n['arch.warning.disabled'].format(
                    bold('pacman')))

            if not self._is_wget_available():
                warnings.append(self.i18n['arch.warning.disabled'].format(
                    bold('wget')))

            if not git.is_enabled():
                warnings.append(self.i18n['arch.warning.git'].format(
                    bold('git')))

        return warnings

    def list_suggestions(self, limit: int) -> List[PackageSuggestion]:
        res = []

        sugs = [(i, p) for i, p in suggestions.ALL.items()]
        sugs.sort(key=lambda t: t[1].value, reverse=True)

        if limit > 0:
            sugs = sugs[0:limit]

        sug_names = {s[0] for s in sugs}

        api_res = self.aur_client.get_info(sug_names)

        if api_res:
            self.categories_mapper.join()
            for pkg in api_res:
                if pkg.get('Name') in sug_names:
                    res.append(
                        PackageSuggestion(
                            self.mapper.map_api_data(pkg, {}, self.categories),
                            suggestions.ALL.get(pkg['Name'])))

        return res

    def is_default_enabled(self) -> bool:
        return False

    def launch(self, pkg: ArchPackage):
        if pkg.command:
            subprocess.Popen(pkg.command.split(' '))

    def get_screenshots(self, pkg: SoftwarePackage) -> List[str]:
        pass