yast/yast-yast2

View on GitHub
library/packages/src/modules/Package.rb

Summary

Maintainability
C
1 day
Test Coverage
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File:  modules/Package.ycp
# Package:  yast2
# Summary:  Packages manipulation
# Authors:  Martin Vidner <mvidner@suse.cz>
#    Michal Svec <msvec@suse.cz>
# Flags:  Stable
#
# $Id$
#
# The documentation is maintained at
# <a href="../index.html">.../docs/index.html</a>.
require "yast"
require "forwardable"
require "y2packager/resolvable"

Yast.import "CommandLine"
Yast.import "Mode"
Yast.import "PackageAI"
Yast.import "PackageSystem"
Yast.import "Popup"

module Yast
  # This module implements support to query, install and remove packages.
  #
  # ## Prefer Package to PackageSystem
  #
  # Depending on the mode, this module decides if it should interact with PackageSystem (libzypp) or
  # PackageAI (AutoYaST). For instance, if you open a module in the AutoYaST UI, calling to
  # {CheckAndInstallPackages} does not install the package for real. Instead, it adds the package to
  # the list of packages to include in the profile. However, when running on other modes (normal,
  # installation, etc.), it just installs the package.
  #
  # ## Overriding default behavior
  #
  # There might a scenario where you want to force Package to work with the real packages. For
  # instance, while reading the configuration during a `clone_system` operation: the mode is still
  # `autoinst_config` but you are dealing with the underlying system. In those cases, you can force
  # {Package} to work with {PackageSystem}.
  #
  # If you are accessing this module through YCP (for instance, using Perl), you cannot pass the
  # :target option. If you need to specify this option, please consider using {PackageSystem} or
  # {PackageAI} functions directly.
  #
  # @example Forcing to check for packages on the underlying system
  #   Yast::Package.Installed("firewalld", target: :system)
  #
  # See https://bugzilla.suse.com/show_bug.cgi?id=1196963 for further details.
  class PackageClass < Module
    extend Forwardable
    include Yast::Logger

    def_delegators :backend, :Available, :PackageAvailable, :DoInstallAndRemove, :InstallKernel

    # @!method Available(package)
    #   Determines whether the package is available or not
    #
    #   This method checks whether any available package provides the given "package".
    #
    #   @param package [String] Package name
    #   @return [Boolean] true if the package is available; false otherwise
    #   @see PackageAvailable

    # @!method PackageAvailable(package)
    #   Determines whether the package  with the given name is available
    #
    #   This method check just the package's name.
    #
    #   @param package [String] Package name
    #   @return [Boolean] true if the package is available; false otherwise
    #   @see Available

    # @!method DoInstall(packages)
    #   Installs the given packages
    #   @param packages [Array<String>] Name of the packages to install
    #   @return [Boolean] true if packages were successfully installed

    # @!method DoRemove(packages)
    #   Removes the given packages
    #   @param packages [Array<String>] Name of the packages to remove
    #   @return [Boolean] true if packages were successfully removed

    # @!method InstallKernel(kernel_modules)
    #   Installs the given kernel modules
    #   @param kernel_modules [Array<String>] Names of the kernel modules to install
    #   @return [Boolean] true on success
    def main
      textdomain "base"

      @last_op_canceled = false
      @installed_packages = []
      @removed_packages = []
    end

    # Determines whether the package is provided or not
    #
    # This method checks whether any installed package provides the given "package".
    #
    # @param package [String] Package name
    # @param target [Symbol,nil] :autoinst or :system. If it is nil,
    #   it guesses the backend depending on the mode.
    # @return [Boolean] true if the package exists; false otherwise
    # @see PackageInstalled
    def Installed(package, target: nil)
      find_backend(target).Installed(package)
    end

    # Determines whether the package is installed or not
    #
    # This method check just the package's name.
    #
    # @param package [String] Package name
    # @param target [Symbol,nil] :autoinst or :system. If it is nil,
    #   it guesses the backend depending on the mode.
    # @return [Boolean] true if the package exists; false otherwise
    # @see Installed
    def PackageInstalled(package, target: nil)
      find_backend(target).PackageInstalled(package)
    end

    # Check if packages are installed
    #
    # Install them if they are not and user approves installation
    #
    # @param packages [Array<String>] list of packages to check (and install)
    # @return [Boolean] true if installation succeeded or packages were installed,
    # false otherwise
    def CheckAndInstallPackages(packages)
      return true if Mode.config
      return true if InstalledAll(packages)

      InstallAll(packages)
    end

    # Check if packages are installed
    #
    #
    # Install them if they are not and user approves installation
    # If installation fails (or wasn't allowed), ask user if he wants to continue
    #
    # @param packages [Array<String>] a list of packages to check (and install)
    # @return [Boolean] true if installation succeeded, packages were installed
    # before or user decided to continue, false otherwise
    def CheckAndInstallPackagesInteractive(packages)
      success = CheckAndInstallPackages(packages)
      return true if success

      if !LastOperationCanceled()
        if Mode.commandline
          # error report
          Report.Error(_("Installing required packages failed."))
        else
          Popup.ContinueCancel(
            # continue/cancel popup
            _(
              "Installing required packages failed. If you continue\n" \
              "without installing required packages,\n" \
              "YaST may not work properly.\n"
            )
          )
        end
      elsif Mode.commandline
        Report.Error(
          # error report
          _("Cannot continue without installing required packages.")
        )
      else
        Popup.ContinueCancel(
          # continue/cancel popup
          _(
            "If you continue without installing required \npackages, YaST may not work properly.\n"
          )
        )
      end
    end

    def DoInstall(packages)
      DoInstallAndRemove(packages, [])
    end

    def DoRemove(packages)
      DoInstallAndRemove([], packages)
    end

    # Install and remove packages in one go
    #
    # @param toinstall [Array<String>] Name of the packages to install
    # @param toremove [Array<String>] Name of the packages to remove
    # @return [Boolean] true on success
    def DoInstallAndRemove(toinstall, toremove)
      ret = backend.DoInstallAndRemove(toinstall, toremove)
      return false unless ret

      if !InstalledAll(toinstall)
        log.error("Required packages have not been installed")
        return false
      end

      true
    end

    def reset
      @installed_packages.clear
      @removed_packages.clear
    end

    # Are all of these packages available?
    # @param [Array<String>] packages list of packages
    # @return [Boolean] true if yes (nil = an error occurred)
    def AvailableAll(packages)
      packages = deep_copy(packages)
      error = false

      which = Builtins.find(packages) do |p|
        avail = Available(p)
        error = true if avail.nil?
        !avail
      end

      return nil if error

      which.nil?
    end

    # Is any of these packages available?
    # @param [Array<String>] packages list of packages
    # @return [Boolean] true if yes (nil = an error occurred)
    def AvailableAny(packages)
      packages = deep_copy(packages)
      error = false

      which = Builtins.find(packages) do |p|
        avail = Available(p)
        error = true if avail.nil?
        avail
      end

      return nil if error

      !which.nil?
    end

    # Are all of these packages installed?
    # @param [Array<String>] packages list of packages
    # @param target [Symbol,nil] :autoinst or :system. If it is nil,
    #   it guesses the backend depending on the mode.
    # @return [Boolean] true if yes
    def InstalledAll(packages, target: nil)
      packages = deep_copy(packages)
      which = Builtins.find(packages) { |p| !Installed(p, target: target) }
      which.nil?
    end

    # Is any of these packages installed?
    # @param [Array<String>] packages list of packages
    # @param target [Symbol,nil] :autoinst or :system. If it is nil,
    #   it guesses the backend depending on the mode.
    # @return [Boolean] true if yes
    def InstalledAny(packages, target: nil)
      packages = deep_copy(packages)
      which = Builtins.find(packages) { |p| Installed(p, target: target) }
      !which.nil?
    end

    # Asks the user if the given packages should be installed or removed
    #
    # It only makes sense in CommandLine mode.
    #
    # @param packs [Array<String>] List of packages to install/remove
    # @param install [Boolean] True to install the packages, false to remove them
    def AskPackages(packs, install)
      packs = deep_copy(packs)
      pkgs = Builtins.mergestring(packs, ", ")
      text = if install
        # the message is followed by list of required packages
        _("These packages need to be installed:")
      else
        # the message is followed by list of required packages
        _("These packages need to be removed:")
      end

      text += " " + pkgs

      CommandLine.Print(text)

      CommandLine.YesNo
    end

    # Main package installatio|removal dialog
    # @param [Array<String>] packages list of packages
    # @param [Boolean] install true if install, false if remove
    # @param [String] message optional installation|removal text (nil -> standard will be used)
    # @return true on success
    def PackageDialog(packages, install, message)
      log.info "Asking for packages: #{packages}"
      packs = Builtins.filter(packages) do |package|
        install ? !Installed(package) : Installed(package)
      end
      log.info "Remaining packages: #{packs}"

      return true if packs.empty?

      check_transactional_system!(packs, install ? :install : :remove)

      # Popup Text
      text = _("These packages need to be installed:") + "<p>"
      # Popup Text
      text = _("These packages need to be removed:") + "<p>" if install == false

      Builtins.foreach(packs) do |p|
        text = Ops.add(text, Builtins.sformat("%1<br>", p))
      end

      text = Builtins.sformat(message, Builtins.mergestring(packs, ", ")) if !message.nil?

      doit = if Mode.commandline
        CommandLine.Interactive ? AskPackages(packs, install) : true
      else
        Popup.AnyQuestionRichText(
          "",
          text,
          40,
          10,
          # labels changed for bug #215195
          #  Label::ContinueButton (), Label::CancelButton (),
          # push button label
          install ? Label.InstallButton : _("&Uninstall"),
          Label.CancelButton,
          :focus_yes
        )
      end

      if doit
        @last_op_canceled = false
        return DoRemove(packs) if install == false

        return DoInstall(packs)
      end

      @last_op_canceled = true
      false
    end

    # Install a package with a custom text message
    # @param [String] package to be installed
    # @param [String] message custom text message
    # @return True on success
    def InstallMsg(package, message)
      PackageDialog([package], true, message)
    end

    # Install list of packages with a custom text message
    # @param [Array<String>] packages The list packages to be installed
    # @param [String] message custom text message
    # @return True on success
    def InstallAllMsg(packages, message)
      packages = deep_copy(packages)
      PackageDialog(packages, true, message)
    end

    # Remove a package with a custom text message
    # @param [String] package  package to be removed
    # @param [String] message custom text message
    # @return True on success
    def RemoveMsg(package, message)
      PackageDialog([package], false, message)
    end

    # Remove a list of packages with a custom text message
    # @param [Array<String>] packages The list of packages to be removed
    # @param [String] message custom text message
    # @return True on success
    def RemoveAllMsg(packages, message)
      packages = deep_copy(packages)
      PackageDialog(packages, false, message)
    end

    # Installs a package
    #
    # @param package [String] package to be installed
    # @return [Boolean] true on success
    def Install(package)
      InstallMsg(package, nil)
    end

    # Installs a list of packages
    #
    # @param packages [Array<String>] list of packages to be installed
    # @return [Boolean] true on success
    def InstallAll(packages)
      packages = deep_copy(packages)
      InstallAllMsg(packages, nil)
    end

    # Removes a package
    #
    # @param package [String] package to be removed
    # @return [Boolean] true on success
    def Remove(package)
      RemoveMsg(package, nil)
    end

    # Removes a list of packages
    #
    # @param packages [Array<String>] list of packages to be removed
    # @return [Boolean] true on success
    def RemoveAll(packages)
      packages = deep_copy(packages)
      RemoveAllMsg(packages, nil)
    end

    # Return result of the last operation
    # Use immediately after calling any Package*:: function
    # @return true if it last operation was canceled
    def LastOperationCanceled
      @last_op_canceled
    end

    # Return if system is transactional and does not support direct package
    # install
    # @return [Boolean]
    def IsTransactionalSystem
      return @transactional unless @transactional.nil?

      mounts = SCR.Read(path(".proc.mounts"))
      root = mounts.find { |m| m["file"] == WFM.scr_root }
      log.info "root in mounts #{root.inspect}"

      raise "Failed to find #{WFM.scr_root} at /proc/mounts" unless root

      # check if there are ro keyword in mount
      @transactional = /(?:^|,)ro(?:,|$)/.match?(root["mntops"])
    end

    publish function: :Available, type: "boolean (string)"
    publish function: :AvailableAll, type: "boolean (list <string>)"
    publish function: :AvailableAny, type: "boolean (list <string>)"
    publish function: :DoInstall, type: "boolean (list <string>)"
    publish function: :DoInstallAndRemove, type: "boolean (list <string>, list <string>)"
    publish function: :DoRemove, type: "boolean (list <string>)"
    publish function: :Install, type: "boolean (string)"
    publish function: :InstallAll, type: "boolean (list <string>)"
    publish function: :InstallAllMsg, type: "boolean (list <string>, string)"
    publish function: :InstallAny, type: "boolean (list <string>)"
    publish function: :InstallAnyMsg, type: "boolean (list <string>, string)"
    publish function: :InstallKernel, type: "boolean (list <string>)"
    publish function: :InstallMsg, type: "boolean (string, string)"
    publish function: :Installed, type: "boolean (string)"
    publish function: :InstalledAll, type: "boolean (list <string>)"
    publish function: :InstalledAny, type: "boolean (list <string>)"
    publish function: :LastOperationCanceled, type: "boolean ()"
    publish function: :PackageAvailable, type: "boolean (string)"
    publish function: :PackageInstalled, type: "boolean (string)"
    publish function: :Remove, type: "boolean (string)"
    publish function: :RemoveAll, type: "boolean (list <string>)"
    publish function: :RemoveAllMsg, type: "boolean (list <string>, string)"
    publish function: :RemoveMsg, type: "boolean (string, string)"
    publish function: :IsTransactionalSystem, type: "boolean ()"

  private

    # Makes sure the package database is initialized.
    def init_packager
      Pkg.TargetInitialize(Installation.destdir)
      Pkg.TargetLoad
      Pkg.SourceRestore
      Pkg.SourceLoad
    end

    # If Yast is running in the autoyast configuration mode
    # no changes will be done on the target system by using
    # the PackageAI class.
    def backend
      Mode.config ? PackageAI : PackageSystem
    end

    # Find the backend for the given target
    #
    # @param target [Symbol,nil] :autoinst or :system. If it is nil,
    #   it guesses the backend depending on the mode.
    def find_backend(target)
      return backend if target.nil?

      found_backend = case target
      when :system
        PackageSystem
      when :autoinst
        PackageAI
      end

      log.warn "select_backend: target '#{target}' is unknown." if found_backend.nil?

      found_backend || backend
    end

    # checks if working on transactional system
    # if so, then it shows popup to user and abort yast
    def check_transactional_system!(packages, mode = :install)
      return unless IsTransactionalSystem()

      msg = _("Transactional system detected. ")
      case mode
      when :install then msg += _("Following packages have to be installed manually:")
      when :remove then msg += _("Following packages have to be removed manually:")
      else
        raise "Unknown mode #{mode}"
      end
      msg += "<p><ul><li>#{packages.join("</li><li>")}</li></ul></p>"
      msg += _("Please start YaST again after reboot.")
      Popup.LongMessage(msg)
      raise Yast::AbortException
    end
  end

  Package = PackageClass.new
  Package.main
end