diff --git a/package/yast2-packager.changes b/package/yast2-packager.changes index 8a69d629d..b64f028f7 100644 --- a/package/yast2-packager.changes +++ b/package/yast2-packager.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Thu May 23 12:03:23 UTC 2019 - Ladislav Slezák + +- Added Y2packager::Resolvables class +- (bsc#1132650) +- 4.2.8 + ------------------------------------------------------------------- Tue May 21 15:19:38 CEST 2019 - schubi@suse.de diff --git a/package/yast2-packager.spec b/package/yast2-packager.spec index b38ce0860..7e068b962 100644 --- a/package/yast2-packager.spec +++ b/package/yast2-packager.spec @@ -17,7 +17,7 @@ Name: yast2-packager -Version: 4.2.7 +Version: 4.2.8 Release: 0 BuildRoot: %{_tmppath}/%{name}-%{version}-build @@ -38,8 +38,8 @@ BuildRequires: yast2-storage-ng >= 4.0.141 # Y2Packager::will_be_obsoleted_by BuildRequires: yast2 >= 4.1.68 -# Pkg::PrdLicenseLocales -BuildRequires: yast2-pkg-bindings >= 4.0.8 +# Pkg::Resolvables +BuildRequires: yast2-pkg-bindings >= 4.2.0 # Augeas lenses BuildRequires: augeas-lenses @@ -47,8 +47,8 @@ BuildRequires: augeas-lenses # Newly added RPM Requires: yast2-country-data >= 2.16.3 -# Pkg::PrdLicenseLocales -Requires: yast2-pkg-bindings >= 4.0.8 +# Pkg::Resolvables +Requires: yast2-pkg-bindings >= 4.2.0 # Y2Packager::will_be_obsoleted_by Requires: yast2 >= 4.1.68 diff --git a/src/lib/y2packager/resolvable.rb b/src/lib/y2packager/resolvable.rb new file mode 100644 index 000000000..812f7f910 --- /dev/null +++ b/src/lib/y2packager/resolvable.rb @@ -0,0 +1,133 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) 2019 SUSE LINUX GmbH, Nuremberg, Germany. +# +# 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" + +Yast.import "Pkg" + +module Y2Packager + # + # This class represents a libzypp resolvable object (package, pattern, patch, + # product, source package) + # + # @note The returned Resolvables might not be valid anymore after changing + # the package manager status (installing/removing packages, changing + # repositories, etc.). After such a change you need to load the resolvables + # again, avoid storing them for later if possible. + # + # @example All installed packages + # Y2Packager::Resolvable.find(kind: :package, status: :installed) + # + # @example Available (not installed) "yast2" packages + # Y2Packager::Resolvable.find(kind: :package, status: :available, name: "yast2") + # + # @example Lazy loading + # res = Y2Packager::Resolvable.find(kind: :package, status: :installed) + # # the `summary` attribute is loaded from libzypp when needed + # res.each {|r| puts "#{r.name} - {r.summary}"} + # + # @example Preloading the attributes + # # the `summary` attribute is loaded from libzypp already at the initial state + # res = Y2Packager::Resolvable.find(kind: :package, status: :installed, [:summary]) + # # this returns the cached `summary` attribute, this is much more efficient + # res.each {|r| puts "#{r.name} - {r.summary}"} + # + # @since 4.2.6 + class Resolvable + include Yast::Logger + + # + # Find the resolvables which match the input parameters. See Yast::Pkg.Resolvables + # + # @param params [Hash] The search filter, only the matching resolvables + # are returned. + # @param preload [Array] The list of attributes which should be preloaded. + # The missing attributes are lazy loaded, however for performance reasons + # you might ask to preload the attributes right at the beginning and avoid + # querying libzypp again later. + # @return [Array] Found resolvables or empty array if nothing found + # @see https://yast-pkg-bindings.surge.sh/ Yast::Pkg.Resolvables + def self.find(params, preload = []) + attrs = (preload + UNIQUE_ATTRIBUTES).uniq + Yast::Pkg.Resolvables(params, attrs).map { |r| new(r) } + end + + # + # Is there any resolvable matching the requested parameters? This is similar to + # the .find method, just instead of a resolvable list it returns a simple Boolean. + # + # @param params [Hash] The requested attributes + # @return [Boolean] `true` if any matching resolvable is found, `false` otherwise. + # @see .find + def self.any?(params) + Yast::Pkg.AnyResolvable(params) + end + + # + # Constructor, initialize the object from a pkg-bindings resolvable hash. + # + # @param hash [Hash] A pkg-bindings resolvable hash. + def initialize(hash) + from_hash(hash) + end + + # + # Dynamically load the missing attributes from libzypp. + # + # @param method [Symbol] the method called + # @param *_args no used so far + # + # @return [] + # + def method_missing(method, *_args) + return instance_variable_get("@#{method}") if instance_variable_defined?("@#{method}") + + # load a missing attribute + if UNIQUE_ATTRIBUTES.all? { |a| instance_variable_defined?("@#{a}") } + load_attribute(method) + else + raise "Missing attributes for identifying the resolvable." + end + end + + private + + # attributes required for identifying a resolvable + UNIQUE_ATTRIBUTES = [:kind, :name, :version, :arch, :source].freeze + + # Load the attributes from a Hash + # + # @param hash [Hash] The resolvable Hash obtained from pkg-bindings. + def from_hash(hash) + hash.each do |k, v| + instance_variable_set("@#{k}", v) + end + end + + # + # Lazy load a missing attribute. + # + # @param attr [Symbol] The required attribute to load. + # @return [Object] The read value. + def load_attribute(attr) + attrs = Hash[(UNIQUE_ATTRIBUTES.map { |a| [a, instance_variable_get("@#{a}")] })] + resolvables = Yast::Pkg.Resolvables(attrs, [attr]) + + # Finding more than one result is suspicious, log a warning + log.warn("Found several resolvables: #{resolvables.inspect}") if resolvables.size > 1 + + resolvable = resolvables.first + raise NoMethodError unless resolvable && resolvable.key?(attr.to_s) + instance_variable_set("@#{attr}", resolvable[attr.to_s]) + end + end +end diff --git a/src/modules/Packages.rb b/src/modules/Packages.rb index 763e0f05c..7fba126c8 100644 --- a/src/modules/Packages.rb +++ b/src/modules/Packages.rb @@ -9,6 +9,7 @@ require "shellwords" require "y2packager/product_upgrade" +require "y2packager/resolvable" # Yast namespace module Yast @@ -812,26 +813,21 @@ def ForceFullRepropose # Reset package selection, but keep the selected objects of the specified type # @param [Array] keep a list of symbols specifying type of objects to be kept selected def Reset(keep) - restore = [] + restore = {} # collect the currently selected resolvables keep.each do |type| - resolvables = Pkg.ResolvableProperties("", type, "") - - resolvables.each do |resolvable| - # only selected items but ignore the selections done by solver, - # during restoration they would be changed to be selected by YaST and they - # will be selected by solver again anyway - next if resolvable["status"] != :selected || resolvable["transact_by"] == :solver - - restore << [resolvable["name"], type] - end + # get the items selected by YaST (the items selected by solver will be later selected again, + # the items selected by user are not reset by the Pkg.PkgApplReset call) + resolvables = Y2Packager::Resolvable.find(kind: type, status: :selected, + transact_by: :app_high) + restore[type] = resolvables.map(&:name) end # This keeps the user-made changes (BNC#446406) Pkg.PkgApplReset - restore.each { |name, type| Pkg.ResolvableInstall(name, type) } + restore.each { |type, list| list.each { |name| Pkg.ResolvableInstall(name, type) } } @system_packages_selected = false diff --git a/test/data/zypp/test_repo/README.md b/test/data/zypp/test_repo/README.md new file mode 100644 index 000000000..ac2a9b9fd --- /dev/null +++ b/test/data/zypp/test_repo/README.md @@ -0,0 +1,4 @@ +The testing data was downloaded from the [YaST:Head OBS]( +https://build.opensuse.org/project/show/YaST:Head) repository. + +- repodata: https://download.opensuse.org/repositories/YaST:/Head/openSUSE_Leap_15.0/repodata/ diff --git a/test/data/zypp/test_repo/repodata/4d295860e625859a714dcc91c6a2e6f7634aab288eb13cf01c09ae2a65a7eeab-primary.xml.gz b/test/data/zypp/test_repo/repodata/4d295860e625859a714dcc91c6a2e6f7634aab288eb13cf01c09ae2a65a7eeab-primary.xml.gz new file mode 100644 index 000000000..bee12362b Binary files /dev/null and b/test/data/zypp/test_repo/repodata/4d295860e625859a714dcc91c6a2e6f7634aab288eb13cf01c09ae2a65a7eeab-primary.xml.gz differ diff --git a/test/data/zypp/test_repo/repodata/repomd.xml b/test/data/zypp/test_repo/repodata/repomd.xml new file mode 100644 index 000000000..6c3176f69 --- /dev/null +++ b/test/data/zypp/test_repo/repodata/repomd.xml @@ -0,0 +1,32 @@ + + + 1558080877 + + obsrepository://build.opensuse.org/YaST:Head/openSUSE_Leap_15.0 + obsbuildid:1533048310 + + + 4d295860e625859a714dcc91c6a2e6f7634aab288eb13cf01c09ae2a65a7eeab + 6c154c0ff76aca4f1b376bbe91f5bf00d56eff19bf5cdf08b680472e42fb4ee1 + + 1558080876 + 88670 + 762992 + + + 1d3843cc83a1bd651295caf1790d9487739a0df387e7cdf1251edbb284f8c387 + 4921d6efe5170264f781275d9ca0408c37d96f262191c143ba6e06562d601820 + + 1558080876 + 239386 + 3058860 + + + 06ae21ca0bb3653b69dc367b078d56d7c73004d1c6dbc0c730a2eff9dd84a9ac + f770a3ae116812d8eeba60b2912d1205175026e1efcefaf899c2d3e03f44888f + + 1558080876 + 230366 + 1439396 + + diff --git a/test/data/zypp/test_repo/repodata/repomd.xml.asc b/test/data/zypp/test_repo/repodata/repomd.xml.asc new file mode 100644 index 000000000..cadeb3446 --- /dev/null +++ b/test/data/zypp/test_repo/repodata/repomd.xml.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.0.7 (GNU/Linux) + +iD8DBQBc3m1tWyVH2GHn0GwRAgZfAJ49sk+alvmPOAGhdAaH7Y4TwpTM6ACZAXnG +Oqgdb6W/cPGRP7c7vIhT/Y8= +=kSvT +-----END PGP SIGNATURE----- diff --git a/test/data/zypp/test_repo/repodata/repomd.xml.key b/test/data/zypp/test_repo/repodata/repomd.xml.key new file mode 100644 index 000000000..6993e1a77 --- /dev/null +++ b/test/data/zypp/test_repo/repodata/repomd.xml.key @@ -0,0 +1,19 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.5 (GNU/Linux) + +mQGiBEfZbkMRBADjWPMrWcXDcSDlPYsnn5tgT1LMJqqJwBisS6fNYWAxEMof4lWJ +MIiRyDE86uAgUvwCsDxmTHeM5uV8KOmsKedxd2bU8lNPSwcSXMLrVhL0mUpPz+EP +eq3VJkLCR+HSszWGkOrh8FddeaQBR5gYiwYu3Spoc8lHiUncbw/N2CeWDwCggMO/ +GsLl+mXbmn4AYFLhNGvRUPUD/2tSlTOAIiGF3Y3svpYdIidMZlNo/+nss0qOMWaM +gZ+3pgXpZ+5pdXbukBupXipEvk+6JnmZjE/sZ/iY2j3WDb9Bh45ix7CA1zXMYtTk +0oAzUmxSWbJVqegcMQkR+9HT1Ys5x+7TQKuQQWXBPtp79zAQlOOa3Vy9Blgnom4R +nue8BACps5N0eQovsbBhGHr9uDcHrYRt74sgLqA83MP6voATCj+NLMFhHgIxJYrt +bItoeEeoPBaJ77AkcMJpitUBW2Ud6bDeChqf48nlnGjVBlIAwx63d0kkYPnsn5kN +BnhkkH9fUDgc934krzsf7dPrpFIkG0LI/TlSshI3HDI71m0r07QqWWFTVCBPQlMg +UHJvamVjdCA8WWFTVEBidWlsZC5vcGVuc3VzZS5vcmc+iGYEExECACYFAlrseIYC +GwMFCRcxukMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRBbJUfYYefQbHQDAJ9Q +4JMWDMaX74S6exNQQiMTdO3whACcDz1jkagJ0BBQY8usbzaJsjCmOm2IRgQTEQIA +BgUCR9luQwAKCRA7MBG3a51lI3yCAJ4jfMqPAn8uPoosOSeA80t7MQdYOACgq9yA +o67MnrLsqTdAM0Ud3ld3NhA= +=JmLZ +-----END PGP PUBLIC KEY BLOCK----- diff --git a/test/packages_test.rb b/test/packages_test.rb index 7e06cc258..49be62909 100755 --- a/test/packages_test.rb +++ b/test/packages_test.rb @@ -1238,27 +1238,11 @@ def product(properties = {}) it "does not select previously unselected items" do allow(Yast::Pkg).to receive(:PkgApplReset) - allow(Yast::Pkg).to receive(:ResolvableProperties).and_return( - [product("name" => "p1", "status" => :selected), product("name" => "p2")] - ) + allow(Y2Packager::Resolvable).to receive(:find) + .with(kind: :product, status: :selected, transact_by: :app_high) + .and_return([Y2Packager::Resolvable.new(product("name" => "p1"))]) expect(Yast::Pkg).to receive(:ResolvableInstall).with("p1", :product) - expect(Yast::Pkg).not_to receive(:ResolvableInstall).with("p2", :product) - - Yast::Packages.Reset([:product]) - end - - # When restoring the selected products ignore the items selected by solver - # (to not change their "transact_by" value). They will be selected again by - # solver if they are still needed. - it "does not restore items selected by solver" do - allow(Yast::Pkg).to receive(:PkgApplReset) - - allow(Yast::Pkg).to receive(:ResolvableProperties).and_return( - [product("name" => "p1", "status" => :selected, "transact_by" => :solver)] - ) - - expect(Yast::Pkg).not_to receive(:ResolvableInstall).with("p1", :product) Yast::Packages.Reset([:product]) end diff --git a/test/resolvable_test.rb b/test/resolvable_test.rb new file mode 100644 index 000000000..c812c7ab2 --- /dev/null +++ b/test/resolvable_test.rb @@ -0,0 +1,98 @@ +#!/usr/bin/env rspec + +require_relative "test_helper" +require "y2packager/resolvable" +require "tmpdir" + +# This is rather an integration test because it actually +# reads a real repository metadata using libzypp. +describe Y2Packager::Resolvable do + before(:all) do + # avoid writing to the protected /var/cache/zypp path + @tmpdir = Dir.mktmpdir + Yast::Pkg.TargetInitialize(@tmpdir) + + # import the repository GPG key + gpg_key = File.join(__dir__, "/data/zypp/test_repo/repodata/repomd.xml.key") + Yast::Pkg.ImportGPGKey(gpg_key, true) + + # add the repository + test_repo = File.join(__dir__, "/data/zypp/test_repo") + Yast::Pkg.SourceCreate("dir:#{test_repo}", "/") + end + + after(:all) do + # close the package manager + Yast::Pkg.SourceFinishAll + Yast::Pkg.TargetFinish + + # remove the tmpdir + FileUtils.remove_entry(@tmpdir) + end + + describe ".find" do + it "finds packages" do + res = Y2Packager::Resolvable.find(kind: :package) + expect(res).to_not be_empty + expect(res.all? { |r| r.kind == :package }).to be true + end + + it "finds packages with a name" do + res = Y2Packager::Resolvable.find(kind: :package, name: "yast2") + expect(res).to_not be_empty + expect(res.all? { |r| r.kind == :package && r.name == "yast2" }).to be true + end + + it "finds patterns" do + res = Y2Packager::Resolvable.find(kind: :pattern) + expect(res).to_not be_empty + expect(res.all? { |r| r.kind == :pattern }).to be true + end + end + + describe ".any?" do + it "returns true if a package is found" do + expect(Y2Packager::Resolvable.any?(kind: :package)).to be true + end + + it "returns true if a package with name is found" do + expect(Y2Packager::Resolvable.any?(kind: :package, name: "yast2")).to be true + end + + it "returns false if a package is not found" do + expect(Y2Packager::Resolvable.any?(kind: :package, name: "not existing")).to be false + end + + it "returns false if a product is not found" do + expect(Y2Packager::Resolvable.any?(kind: :product)).to be false + end + end + + describe "#vendor" do + it "lazy loads the missing attributes" do + res = Y2Packager::Resolvable.find(kind: :package, name: "yast2").first + expect(Yast::Pkg).to receive(:Resolvables).with(anything, [:vendor]).and_call_original + expect(res.vendor).to eq("obs://build.opensuse.org/YaST") + end + + it "does not load the preloaded attributes again" do + res = Y2Packager::Resolvable.find({ kind: :package, name: "yast2" }, [:vendor]).first + expect(Yast::Pkg).to_not receive(:Resolvables).with(anything, [:vendor]) + expect(res.vendor).to eq("obs://build.opensuse.org/YaST") + end + + it "raises an exception if the resolvable cannot be uniquely identified for lazy loading" do + # this a bit artificial situation, create a Resolvable from an incomplete hash + # (missing version and other attributes) + res = Y2Packager::Resolvable.new(kind: :package, name: "yast2") + expect { res.vendor }.to raise_error(RuntimeError, /missing attributes/i) + end + end + + describe "#method_missing" do + it "raises NoMethodError when the attribute does not exist" do + res = Y2Packager::Resolvable.find(kind: :package, name: "yast2").first + expect { res.not_existing_method }.to raise_error(NoMethodError) + end + end +end