Files
entropy/client/solo/commands/install.py
T
Sławomir Nizio 7cf7e1f6b7 convert solo imports, with needed changes to entropy_path_loader
The module entropy_path_loader (used only for running from within the
checkout; otherwise not even installed) is made to provide the _entropy
namespace.

(Other ideas instead of this entropy_path_loader change would be to
reorganise files layout; drop support for running from the checkout as
is - and perhaps require virtualenvs; require sourcing a script that
sets PYTHONPATH. However, as implemented, it is not intrusive, and the
good part is that it is quite isolated, not used in normal usage after
installation. Basically, it only does sys.path + provides _entropy
namespace.)
2018-11-26 20:15:36 +01:00

737 lines
25 KiB
Python

# -*- coding: utf-8 -*-
"""
@author: Fabio Erculiani <lxnay@sabayon.org>
@contact: lxnay@sabayon.org
@copyright: Fabio Erculiani
@license: GPL-2
B{Entropy Command Line Client}.
"""
import argparse
import os
import sys
from entropy.i18n import _
from entropy.const import etpConst, const_convert_to_unicode
from entropy.locks import UpdatesNotificationResourceLock
from entropy.misc import ParallelTask
from entropy.output import brown, purple, darkred, red, \
blue, darkblue, darkgreen, bold
from entropy.client.interfaces.package.actions.action import PackageAction
import entropy.tools
from _entropy.solo.utils import enlightenatom
from _entropy.solo.commands.descriptor import SoloCommandDescriptor
from _entropy.solo.commands._manage import SoloManage
class SoloInstall(SoloManage):
"""
Main Solo Install command.
"""
NAME = "install"
ALIASES = ["i"]
ALLOW_UNPRIVILEGED = False
INTRODUCTION = """\
Install or update packages or package files.
"""
SEE_ALSO = "equo-remove(1), equo-config(1)"
def __init__(self, args):
SoloManage.__init__(self, args)
self._commands = {}
self._check_critical_updates = True
def _get_parser(self):
"""
Overridden from SoloCommand.
"""
_commands = {}
descriptor = SoloCommandDescriptor.obtain_descriptor(
SoloInstall.NAME)
parser = argparse.ArgumentParser(
description=descriptor.get_description(),
formatter_class=argparse.RawDescriptionHelpFormatter,
prog="%s %s" % (sys.argv[0], SoloInstall.NAME))
parser.set_defaults(func=self._install)
parser.add_argument(
"packages", nargs='+',
metavar="<package>", help=_("package name"))
mg_group = parser.add_mutually_exclusive_group()
mg_group.add_argument(
"--ask", "-a", action="store_true",
default=False,
help=_("ask before making any changes"))
_commands["--ask"] = {}
_commands["-a"] = {}
mg_group.add_argument(
"--pretend", "-p", action="store_true",
default=False,
help=_("show what would be done"))
_commands["--pretend"] = {}
_commands["-p"] = {}
parser.add_argument(
"--verbose", "-v", action="store_true",
default=False,
help=_("verbose output"))
_commands["--verbose"] = {}
_commands["-v"] = {}
parser.add_argument(
"--quiet", "-q", action="store_true",
default=False,
help=_("quiet output"))
_commands["--quiet"] = {}
_commands["-q"] = {}
parser.add_argument(
"--fetch", action="store_true",
default=False,
help=_("just download packages"))
_commands["--fetch"] = {}
parser.add_argument(
"--bdeps", action="store_true",
default=False,
help=_("include build-time dependencies"))
_commands["--bdeps"] = {}
parser.add_argument(
"--nodeps", action="store_true",
default=False,
help=_("exclude package dependencies"))
_commands["--nodeps"] = {}
parser.add_argument(
"--onlydeps", "-o", action="store_true",
default=False,
help=_("only include dependencies of selected packages"))
_commands["--onlydeps"] = {}
_commands["-o"] = {}
parser.add_argument(
"--norecursive", action="store_true",
default=False,
help=_("do not calculate dependencies recursively"))
_commands["--norecursive"] = {}
parser.add_argument(
"--deep", action="store_true",
default=False,
help=_("include dependencies no longer needed"))
_commands["--deep"] = {}
parser.add_argument(
"--empty", action="store_true",
default=False,
help=_("when used with --deep, include virtual packages"))
_commands["--empty"] = {}
parser.add_argument(
"--configfiles", action="store_true",
default=False,
help=_("remove package configuration files no longer needed"))
_commands["--configfiles"] = {}
parser.add_argument(
"--relaxed", action="store_true",
default=False,
help=_("relax dependencies constraints during calculation"))
_commands["--relaxed"] = {}
parser.add_argument(
"--multifetch",
type=int, default=1,
choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
help=_("download multiple packages in parallel (max 10)"))
_commands["--multifetch"] = {}
self._commands = _commands
return parser
def bashcomp(self, last_arg):
"""
Overridden from SoloCommand.
"""
self._get_parser() # this will generate self._commands
return self._hierarchical_bashcomp(last_arg, [], self._commands)
def _install(self, entropy_client):
"""
Solo Install command.
"""
deps = not self._nsargs.nodeps
recursive = not self._nsargs.norecursive
pretend = self._nsargs.pretend
ask = self._nsargs.ask
verbose = self._nsargs.verbose
quiet = self._nsargs.quiet
empty = self._nsargs.empty
config_files = self._nsargs.configfiles
deep = self._nsargs.deep
fetch = self._nsargs.fetch
bdeps = self._nsargs.bdeps
onlydeps = self._nsargs.onlydeps
relaxed = self._nsargs.relaxed
multifetch = self._nsargs.multifetch
packages = self._nsargs.packages
exit_st, _show_cfgupd = self._install_action(
entropy_client, deps, recursive,
pretend, ask, verbose, quiet, empty,
config_files, deep, fetch, bdeps, onlydeps,
relaxed, multifetch, packages)
if _show_cfgupd:
self._show_config_files_update(entropy_client)
self._show_preserved_libraries(entropy_client)
return exit_st
@staticmethod
def _match_pkg_in_installed_repo(repo, package_id, inst_repo):
"""
Match package in installed repository by name and retrieved slot.
"""
atom = repo.retrieveAtom(package_id)
pkgslot = repo.retrieveSlot(package_id)
inst_pkg_id, inst_pkg_rc = inst_repo.atomMatch(
entropy.dep.dep_getkey(atom), matchSlot = pkgslot)
return inst_pkg_id, inst_pkg_rc
@classmethod
def _get_installed_packages_sources(cls, entropy_client,
inst_repo, run_queue):
"""
Return a dict whose keys are installed packages among the ones in
run_queue and values are installed sources.
"""
inst_sources = {}
for package_id, repository_id in run_queue:
repo = entropy_client.open_repository(repository_id)
inst_pkg_id, inst_pkg_rc = cls._match_pkg_in_installed_repo(
repo, package_id, inst_repo)
if inst_pkg_rc == 0:
source_id = inst_repo.getInstalledPackageSource(inst_pkg_id)
inst_sources[(package_id, repository_id)] = source_id
return inst_sources
@classmethod
def _show_install_queue(cls, entropy_client, inst_repo,
run_queue, removal_queue, ask, pretend,
quiet, verbose):
"""
Show expanded installation queue to user.
"""
download_size = 0
unpack_size = 0
on_disk_used_size = 0
on_disk_freed_size = 0
pkgs_install = 0
pkgs_update = 0
pkgs_reinstall = 0
pkgs_downgrade = 0
pkgs_remove = len(removal_queue)
client_settings = entropy_client.ClientSettings()
splitdebug = client_settings['misc']['splitdebug']
if run_queue and ((ask or pretend) and not quiet):
inst_msg = _("These are the packages that would be installed")
entropy_client.output(
"%s:" % (blue(inst_msg),),
header=darkred(" @@ "))
for package_id, repository_id in run_queue:
repo = entropy_client.open_repository(repository_id)
atom = repo.retrieveAtom(package_id)
pkgver = repo.retrieveVersion(package_id)
pkgtag = repo.retrieveTag(package_id)
pkgrev = repo.retrieveRevision(package_id)
pkgfile = repo.retrieveDownloadURL(package_id)
on_disk_used_size += repo.retrieveOnDiskSize(package_id)
pkgsize = repo.retrieveSize(package_id)
extra_downloads = repo.retrieveExtraDownload(package_id)
for extra_download in extra_downloads:
if not splitdebug and (extra_download['type'] == "debug"):
continue
pkgsize += extra_download['size']
on_disk_used_size += extra_download['disksize']
unpack_size += int(pkgsize) * 2
fetch_path = PackageAction.get_standard_fetch_disk_path(pkgfile)
if not os.path.exists(fetch_path):
download_size += int(pkgsize)
else:
try:
f_size = entropy.tools.get_file_size(fetch_path)
except OSError:
f_size = 0
download_size += pkgsize - f_size
installed_ver = '-1'
installed_tag = ''
installed_rev = 0
inst_repo_s = None
inst_pkg_id, inst_pkg_rc = cls._match_pkg_in_installed_repo(
repo, package_id, inst_repo)
if inst_pkg_rc == 0:
installed_ver = inst_repo.retrieveVersion(
inst_pkg_id)
installed_tag = inst_repo.retrieveTag(
inst_pkg_id)
installed_rev = inst_repo.retrieveRevision(
inst_pkg_id)
inst_repo_s = \
inst_repo.getInstalledPackageRepository(
inst_pkg_id)
if inst_repo_s is None:
inst_repo_s = _("Not available")
on_disk_freed_size += inst_repo.retrieveOnDiskSize(
inst_pkg_id)
extra_downloads = inst_repo.retrieveExtraDownload(
inst_pkg_id)
for extra_download in extra_downloads:
on_disk_freed_size += extra_download['disksize']
# statistics generation complete
# if --quiet, we're done doing stuff
if quiet:
continue
inst_meta = (installed_ver, installed_tag, installed_rev,)
avail_meta = (pkgver, pkgtag, pkgrev,)
action = 0
repo_switch = False
if (repository_id != inst_repo_s) and \
(inst_repo_s is not None):
repo_switch = True
if repo_switch:
flags = darkred(" [")
else:
flags = " ["
if inst_repo_s is None:
inst_repo_s = _('Not available')
pkgcmp = entropy_client.get_package_action(
(package_id, repository_id))
if pkgcmp == 0:
pkgs_reinstall += 1
flags += red("R")
action = 1
elif pkgcmp == 1:
pkgs_install += 1
flags += darkgreen("N")
elif pkgcmp == 2:
pkgs_update += 1
if avail_meta == inst_meta:
flags += blue("U") + red("R")
else:
flags += blue("U")
action = 2
else:
pkgs_downgrade += 1
flags += darkblue("D")
action = -1
if repo_switch:
flags += darkred("] ")
else:
flags += "] "
if repo_switch:
repo_info = "[%s->%s] " % (
brown(inst_repo_s),
darkred(repository_id),)
else:
repo_info = "[%s] " % (
brown(repository_id),)
old_info = ""
if action != 0:
old_info = " [%s|%s" % (
blue(installed_ver),
darkred(const_convert_to_unicode(installed_rev)),)
old_tag = "]"
if installed_tag:
old_tag = "|%s%s" % (
darkred(installed_tag),
old_tag,)
old_info += old_tag
entropy_client.output(
"%s%s%s|%s%s" % (
flags,
repo_info,
enlightenatom(atom),
darkred(const_convert_to_unicode(pkgrev)),
old_info,),
header=darkred(" ##"))
delta_size = on_disk_used_size - on_disk_freed_size
needed_size = delta_size
if unpack_size > 0:
needed_size += unpack_size
if (ask or pretend or verbose) and removal_queue:
mytxt = "%s (%s):" % (
blue(_("These are the packages that would be removed")),
bold(_("conflicting/substituted")),
)
entropy_client.output(
mytxt, header=darkred(" @@ "))
for package_id in removal_queue:
atom = inst_repo.retrieveAtom(package_id)
on_disk_freed_size += inst_repo.retrieveOnDiskSize(
package_id)
extra_downloads = inst_repo.retrieveExtraDownload(
package_id)
for extra_download in extra_downloads:
on_disk_freed_size += extra_download['disksize']
installedfrom = inst_repo.getInstalledPackageRepository(
package_id)
if installedfrom is None:
installedfrom = _("Not available")
mytxt = "[%s] %s%s: %s%s %s" % (
purple("W"),
darkred("["),
brown(_("from")),
bold(installedfrom),
darkred("]"),
enlightenatom(atom))
entropy_client.output(mytxt, header=darkred(" ## "))
# if --quiet, there is nothing else to show
if quiet:
return
mytxt = "%s: %s" % (
blue(_("Packages needing to be installed/updated/downgraded")),
darkred(const_convert_to_unicode(len(run_queue))),)
entropy_client.output(mytxt, header=darkred(" @@ "))
mytxt = "%s: %s" % (
blue(_("Packages needing to be removed")),
darkred(const_convert_to_unicode(pkgs_remove)),)
entropy_client.output(mytxt, header=darkred(" @@ "))
if ask or verbose or pretend:
mytxt = "%s: %s" % (
darkgreen(_("Packages needing to be installed")),
darkgreen(const_convert_to_unicode(pkgs_install)),
)
entropy_client.output(
mytxt, header=darkred(" @@ "))
mytxt = "%s: %s" % (
brown(_("Packages needing to be reinstalled")),
brown(const_convert_to_unicode(pkgs_reinstall)),
)
entropy_client.output(
mytxt, header=darkred(" @@ "))
mytxt = "%s: %s" % (
blue(_("Packages needing to be updated")),
blue(const_convert_to_unicode(pkgs_update)),
)
entropy_client.output(
mytxt, header=darkred(" @@ "))
mytxt = "%s: %s" % (
darkred(_("Packages needing to be downgraded")),
darkred(const_convert_to_unicode(pkgs_downgrade)),
)
entropy_client.output(
mytxt, header=darkred(" @@ "))
if download_size > 0:
mysize = const_convert_to_unicode(
entropy.tools.bytes_into_human(download_size))
else:
mysize = const_convert_to_unicode("0b")
mytxt = "%s: %s" % (
blue(_("Download size")),
bold(mysize),
)
entropy_client.output(
mytxt, header=darkred(" @@ "))
if delta_size > 0:
mysizetxt = _("Used disk space")
else:
mysizetxt = _("Freed disk space")
delta_size = -delta_size
delta_human = entropy.tools.bytes_into_human(delta_size)
mytxt = "%s: %s" % (
blue(mysizetxt),
bold(delta_human),
)
entropy_client.output(mytxt, header=darkred(" @@ "))
if needed_size < 0:
needed_size = -needed_size
mytxt = "%s: %s %s" % (
blue(_("You need at least")),
bold(entropy.tools.bytes_into_human(needed_size)),
blue(_("of free space")),
)
entropy_client.output(
mytxt, header=darkred(" @@ "))
# check for disk space and print a warning
target_dir = etpConst['entropyunpackdir']
while not os.path.isdir(target_dir):
target_dir = os.path.dirname(target_dir)
size_match = entropy.tools.check_required_space(target_dir,
needed_size)
if not size_match:
mytxt = "%s: %s" % (
blue(_("You don't have enough space for "
"the installation. Free some space into")),
darkred(target_dir),)
entropy_client.output(
bold(_("Attention")),
header=darkred(" !!! "))
entropy_client.output(
bold(_("Attention")),
header=darkred(" !!! "))
entropy_client.output(
mytxt, header=darkred(" !!! "))
entropy_client.output(
bold(_("Attention")),
header=darkred(" !!! "))
entropy_client.output(
bold(_("Attention")),
header=darkred(" !!! "))
def _match_packages_for_installation(self, entropy_client,
onlydeps, packages):
"""
Converts packages from user to package_matches that
can be used in _install_action.
"""
package_matches = self._scan_packages(
entropy_client, packages,
onlydeps=onlydeps)
if not package_matches:
entropy_client.output(
"%s." % (
darkred(_("No package_matches found")),),
level="error", importance=1)
return package_matches
def _install_action(self, entropy_client, deps, recursive,
pretend, ask, verbose, quiet, empty,
config_files, deep, fetch, bdeps,
onlydeps, relaxed, multifetch, packages,
package_matches=None):
"""
Solo Install action implementation.
Packages passed in the packages argument (as opposed to
package_matches) will be marked as installed by user.
"""
inst_repo = entropy_client.installed_repository()
action_factory = entropy_client.PackageActionFactory()
packages_by_user = set()
with inst_repo.shared():
self._advise_repository_update(entropy_client)
if self._check_critical_updates:
self._advise_packages_update(entropy_client)
if package_matches is None:
packages = self._match_packages_for_installation(
entropy_client, onlydeps, packages)
if not packages:
return 1, False
packages_by_user = set(packages)
else:
packages = package_matches
run_queue, removal_queue = self._generate_install_queue(
entropy_client, packages, deps, empty, deep, relaxed,
onlydeps, bdeps, recursive)
if (run_queue is None) or (removal_queue is None):
return 1, False
elif not (run_queue or removal_queue):
entropy_client.output(
"%s." % (blue(_("Nothing to do")),),
level="warning", header=darkgreen(" @@ "))
return 0, True
self._show_install_queue(
entropy_client, inst_repo,
run_queue, removal_queue, ask, pretend, quiet, verbose)
installed_pkg_sources = self._get_installed_packages_sources(
entropy_client, inst_repo, run_queue)
if ask:
rc = entropy_client.ask_question(
" %s" % (_("Would you like to continue ?"),))
if rc == _("No"):
return 1, False
if pretend:
return 0, True # yes, tell user
if self._interactive:
exit_st = self._accept_license(
entropy_client, inst_repo, run_queue)
if exit_st != 0:
return 1, False
ugc_thread = None
down_data = {}
exit_st = self._download_packages(
entropy_client, run_queue, down_data, multifetch)
if exit_st == 0:
ugc_thread = ParallelTask(
self._signal_ugc, entropy_client, down_data)
ugc_thread.name = "UgcThread"
ugc_thread.start()
elif exit_st != 0:
return 1, False
# is --fetch on? then quit.
if fetch:
if ugc_thread is not None:
ugc_thread.join()
entropy_client.output(
"%s." % (
blue(_("Download complete")),),
header=darkred(" @@ "))
return 0, False
notification_lock = UpdatesNotificationResourceLock(
output=entropy_client)
total = len(run_queue)
notif_acquired = False
try:
# this is a best effort, we will not sleep if the lock
# is not acquired because we may get blocked for an eternity
# (well, for a very long time) in this scenario:
# 1. RigoDaemon is running some action queue
# 2. Another thread in RigoDaemon is stuck on the activity
# mutex with the notification lock held.
# 3. We cannot move on here because of 2.
# Nothing bad will happen if we just ignore the acquisition
# state.
notif_acquired = notification_lock.try_acquire_shared()
metaopts = {
'removeconfig': config_files,
# This can be used by PackageAction based classes
# to know what's the overall package schedule for
# both upgrade and install actions. This way, we
# can better handle conflicts.
'install_queue' : run_queue,
}
for count, pkg_match in enumerate(run_queue, 1):
source_id = installed_pkg_sources.get(pkg_match, None)
if not onlydeps and pkg_match in packages_by_user:
metaopts['install_source'] = \
etpConst['install_sources']['user']
elif source_id is not None:
# Retain the information.
# Install action can upgrade packages, their source
# should not be changed to automatic_dependency.
metaopts['install_source'] = source_id
else:
metaopts['install_source'] = \
etpConst['install_sources']['automatic_dependency']
package_id, repository_id = pkg_match
atom = entropy_client.open_repository(
repository_id).retrieveAtom(package_id)
pkg = None
try:
pkg = action_factory.get(
action_factory.INSTALL_ACTION,
pkg_match, opts=metaopts)
xterm_header = "equo (%s) :: %d of %d ::" % (
_("install"), count, total)
pkg.set_xterm_header(xterm_header)
entropy_client.output(
purple(atom),
count=(count, total),
header=darkgreen(" +++ ") + ">>> ")
exit_st = pkg.start()
if exit_st != 0:
if ugc_thread is not None:
ugc_thread.join()
return 1, True
finally:
if pkg is not None:
pkg.finalize()
finally:
if notif_acquired:
notification_lock.release()
if ugc_thread is not None:
ugc_thread.join()
entropy_client.output(
"%s." % (
blue(_("Installation complete")),),
header=darkred(" @@ "))
return 0, True
SoloCommandDescriptor.register(
SoloCommandDescriptor(
SoloInstall,
SoloInstall.NAME,
_("install or update packages or package files"))
)