library/packages/src/lib/y2packager/product_reader.rb
# ------------------------------------------------------------------------------
# Copyright (c) 2017 SUSE LLC, 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.
# ------------------------------------------------------------------------------
require "yast"
require "y2packager/product"
require "y2packager/product_sorter"
require "y2packager/resolvable"
Yast.import "Pkg"
Yast.import "Linuxrc"
Yast.import "Stage"
module Y2Packager
# Read the product information from libzypp
class ProductReader
include Yast::Logger
class << self
# Installation packages map
#
# This map contains the correspondence between products and the
# installation package for each product.
#
# The information is always read again. Reason is that that url can be invalid,
# but user fix it later. This way it cache invalid result. See bsc#1086840
# ProductReader instance cache it properly, but caching for installation life-time
# should be prevented.
#
# @return [Hash<String,String>] product name -> installation package name
def installation_package_mapping
installation_packages = Yast::Pkg.PkgQueryProvides("system-installation()")
log.info "Installation packages: #{installation_packages.inspect}"
installation_package_mapping = {}
installation_packages.each do |list|
pkg_name = list.first
# There can be more instances of same package in different version.
# Prefer the selected or the available package, they should provide newer data
# than the installed one.
packages = Yast::Pkg.Resolvables({ name: pkg_name, kind: :package }, [:dependencies, :status])
package = packages.find { |p| p["status"] == :selected } ||
packages.find { |p| p["status"] == :available } ||
packages.first
dependencies = package["deps"]
install_provides = dependencies.find_all do |d|
d["provides"]&.match(/system-installation\(\)/)
end
# parse product name from provides. Format of provide is
# `system-installation() = <product_name>`
install_provides.each do |install_provide|
product_name = install_provide["provides"][/system-installation\(\)\s*=\s*(\S+)/, 1]
log.info "package #{pkg_name} install product #{product_name}"
installation_package_mapping[product_name] = pkg_name
end
end
installation_package_mapping
end
end
# Available products
#
# @return [Array<Product>] Available products
def all_products(force_repos: false)
linuxrc_special_products = if Yast::Linuxrc.InstallInf("specialproduct")
linuxrc_string(Yast::Linuxrc.InstallInf("specialproduct")).split(",")
else
[]
end
return @all_products if @all_products && !force_repos
@all_products = []
available_products.each do |prod|
prod_pkg = product_package(prod.product_package)
if prod_pkg
# remove special products if they have not been defined in linuxrc
prod_pkg.deps.find { |dep| dep["provides"] =~ /\Aspecialproduct\(\s*(.*?)\s*\)\z/ }
special_product_tag = linuxrc_string(Regexp.last_match[1]) if Regexp.last_match
if special_product_tag && !linuxrc_special_products.include?(special_product_tag)
log.info "Special product #{prod.name} has not been defined via linuxrc. --> do not offer it"
next
end
# Evaluating display order
prod_pkg.deps.find { |dep| dep["provides"] =~ /\Adisplayorder\(\s*([0-9]+)\s*\)\z/ }
displayorder = Regexp.last_match[1].to_i if Regexp.last_match
end
@all_products << Y2Packager::Product.from_resolvable(
prod, installation_package_mapping[prod.name], displayorder
)
end
@all_products
end
# In installation Read the available libzypp base products for installation
# @return [Array<Y2Packager::Product>] the found available base products,
# the products are sorted by the 'displayorder' provides value
def available_base_products(force_repos: false)
# If no product contains a 'system-installation()' tag but there is only 1 product,
# we assume that it is the base one.
products = all_products(force_repos: force_repos)
if products.size == 1 && installation_package_mapping.empty?
log.info "Assuming that #{products.inspect} is the base product."
return products
end
log.info "all products #{products}"
# only installable products
products = products.select(&:installation_package).sort(&::Y2Packager::PRODUCT_SORTER)
log.info "available base products #{products}"
products
end
# Read the installed base product
# @return [Y2Packager::Product,nil] the installed base product or nil if not found
def installed_base_product
base = base_product
return nil unless base
Y2Packager::Product.from_resolvable(base, installation_package_mapping[base.name])
end
# All installed products
# @return [Array<Y2Packager::Product>] the product list
def all_installed_products
installed_products.map do |p|
Y2Packager::Product.from_resolvable(p, installation_package_mapping[p.name])
end
end
def product_package(name, _repo_id = nil)
return nil unless name
# find the highest version
Y2Packager::Resolvable.find(kind: :package, name: name).reduce(nil) do |a, p|
(!a || (Yast::Pkg.CompareVersions(a.version, p.version) < 0)) ? p : a
end
end
private
# read the available products, remove potential duplicates
# @return [Array<Hash>] pkg-bindings data structure
def zypp_products
products = Y2Packager::Resolvable.find({ kind: :product }, [:register_target])
# remove duplicates, there might be different flavors ("DVD"/"POOL")
# or archs (x86_64/i586), when selecting the product to install later
# libzypp will select the correct arch automatically,
# keep products with different location, they are filtered out later
products.uniq! { |p| "#{p.name}__#{p.version}__#{resolvable_location(p)}" }
log.info "Found products: #{products.map(&:name)}"
products
end
# read the available products, remove potential duplicates
# @return [Array<Y2Packager::Resolvable>] list of products
def available_products
# select only the available or to be installed products
zypp_products.select { |p| p.status == :available || p.status == :selected }
end
# read the installed products
# @return [Array<Y2Packager::Resolvable>] list of products
def installed_products
# select only the installed or to be removed products
zypp_products.select { |p| p.status == :installed || p.status == :removed }
end
# find the installed base product
# @return[Y2Packager::Resolvable,nil] the pkg-bindings product structure or nil if not found
def base_product
# The base product is identified by the /etc/products.d/baseproduct symlink
# and because a symlink can point only to one file there can be only one base product.
# The "installed" condition is actually not required because that symlink is created
# only for installed products. (Just make sure it still works in case the libzypp
# internal implementation is changed.)
base = installed_products.find { |p| p.type == "base" }
log.info("Found installed base product: #{base&.name}")
base
end
def installation_package_mapping
@installation_package_mapping ||= self.class.installation_package_mapping
end
# Process the string in a linuxrc way: remove the "-", "_", "." characters,
# convert it to downcase for case insensitive comparison.
#
# @param input [String] the input string
#
# @return [String] the processed string
#
def linuxrc_string(input)
return nil if input.nil?
ret = input.gsub(/[-_.]/, "")
ret.downcase
end
#
# Evaluate the resolvable location (on system or on media).
#
# @param res [Hash] the resolvable hash obtained from pkg-bindings
#
# @return [Symbol] `:on_medium` or `:on_system`
#
def resolvable_location(res)
case res.status
when :available, :selected
:on_medium
when :installed, :removed
:on_system
else
# just in case pkg-bindings add some new status...
raise "Unexpected resolvable status: #{res.status}"
end
end
end
end