From e4ed209d949033980e1e4781031dfd7c9360f523 Mon Sep 17 00:00:00 2001 From: Animesh Sahu Date: Wed, 24 Apr 2024 12:24:12 +0530 Subject: [PATCH] (maint) Add xbps used by voidlinux as a package provider --- lib/puppet/provider/package/xbps.rb | 127 +++++++++++++++++++++ spec/unit/provider/package/xbps_spec.rb | 143 ++++++++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 lib/puppet/provider/package/xbps.rb create mode 100644 spec/unit/provider/package/xbps_spec.rb diff --git a/lib/puppet/provider/package/xbps.rb b/lib/puppet/provider/package/xbps.rb new file mode 100644 index 00000000000..1a3cd4a9e01 --- /dev/null +++ b/lib/puppet/provider/package/xbps.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require_relative "../../../puppet/provider/package" + +Puppet::Type.type(:package).provide :xbps, :parent => Puppet::Provider::Package do + desc "Support for the Package Manager Utility (xbps) used in VoidLinux. + + This provider supports the `install_options` attribute, which allows command-line flags to be passed to xbps-install. + These options should be specified as an array where each element is either a string or a hash." + + commands :xbps_install => "/usr/bin/xbps-install" + commands :xbps_remove => "/usr/bin/xbps-remove" + commands :xbps_query => "/usr/bin/xbps-query" + commands :xbps_pkgdb => "/usr/bin/xbps-pkgdb" + + confine 'os.name' => :void + defaultfor 'os.name' => :void + has_feature :install_options, :uninstall_options, :upgradeable, :holdable, :virtual_packages + + def self.defaultto_allow_virtual + false + end + + # Fetch the list of packages that are currently installed on the system. + def self.instances + packages = [] + execpipe([command(:xbps_query), "-l"]) do |pipe| + # xbps-query -l output is 'ii package-name-version desc' + regex = /^\S+\s(\S+)-(\S+)\s+\S+/ + pipe.each_line do |line| + match = regex.match(line.chomp) + if match + packages << new({ name: match.captures[0], ensure: match.captures[1], provider: name }) + else + warning(_("Failed to match line '%{line}'") % { line: line }) + end + end + end + + packages + rescue Puppet::ExecutionFailure + fail(_("Error getting installed packages")) + end + + # Install a package quietly (without confirmation or progress bar) using 'xbps-install'. + def install + resource_name = @resource[:name] + resource_source = @resource[:source] + + cmd = %w[-S -y] + cmd += install_options if @resource[:install_options] + cmd << "--repository=#{resource_source}" if resource_source + cmd << resource_name + + unhold if properties[:mark] == :hold + begin + xbps_install(*cmd) + ensure + hold if @resource[:mark] == :hold + end + end + + # Because Voidlinux is a rolling release based distro, installing a package + # should always result in the newest release. + def update + install + end + + # Removes a package from the system. + def uninstall + resource_name = @resource[:name] + + cmd = %w[-R -y] + cmd += uninstall_options if @resource[:uninstall_options] + cmd << resource_name + + xbps_remove(*cmd) + end + + # The latest version of a given package + def latest + query&.[] :ensure + end + + # Queries information for a package + def query + resource_name = @resource[:name] + installed_packages = self.class.instances + + installed_packages.each do |pkg| + return pkg.properties if @resource[:name].casecmp(pkg.name).zero? + end + + return nil unless @resource.allow_virtual? + + # Search for virtual package + output = xbps_query("-Rs", resource_name).chomp + + # xbps-query -Rs output is '[*] package-name-version description' + regex = /^\[\*\]+\s(\S+)-(\S+)\s+\S+/ + match = regex.match(output) + + return nil unless match + + { name: match.captures[0], ensure: match.captures[1], provider: self.class.name } + end + + # Puts a package on hold, so it doesn't update by itself on system update + def hold + xbps_pkgdb("-m", "hold", @resource[:name]) + end + + # Puts a package out of hold + def unhold + xbps_pkgdb("-m", "unhold", @resource[:name]) + end + + private + + def install_options + join_options(@resource[:install_options]) + end + + def uninstall_options + join_options(@resource[:uninstall_options]) + end +end diff --git a/spec/unit/provider/package/xbps_spec.rb b/spec/unit/provider/package/xbps_spec.rb new file mode 100644 index 00000000000..2c769541793 --- /dev/null +++ b/spec/unit/provider/package/xbps_spec.rb @@ -0,0 +1,143 @@ +require "spec_helper" +require "stringio" + +describe Puppet::Type.type(:package).provider(:xbps) do + before do + @resource = Puppet::Type.type(:package).new(name: "gcc", provider: "xbps") + @provider = described_class.new(@resource) + @resolver = Puppet::Util + + allow(described_class).to receive(:which).with("/usr/bin/xbps-install").and_return("/usr/bin/xbps-install") + allow(described_class).to receive(:which).with("/usr/bin/xbps-remove").and_return("/usr/bin/xbps-remove") + allow(described_class).to receive(:which).with("/usr/bin/xbps-query").and_return("/usr/bin/xbps-query") + end + + it { is_expected.to be_installable } + it { is_expected.to be_uninstallable } + it { is_expected.to be_install_options } + it { is_expected.to be_uninstall_options } + it { is_expected.to be_upgradeable } + it { is_expected.to be_holdable } + it { is_expected.to be_virtual_packages } + + it "should be the default provider on 'os.name' => Void" do + expect(Facter).to receive(:value).with('os.name').and_return("Void") + expect(described_class.default?).to be_truthy + end + + describe "when determining instances" do + it "should return installed packages" do + sample_installed_packages = %{ +ii gcc-12.2.0_1 GNU Compiler Collection +ii ruby-devel-3.1.3_1 Ruby programming language - development files +} + + expect(described_class).to receive(:execpipe).with(["/usr/bin/xbps-query", "-l"]) + .and_yield(StringIO.new(sample_installed_packages)) + + instances = described_class.instances + expect(instances.length).to eq(2) + + expect(instances[0].properties).to eq({ + :name => "gcc", + :ensure => "12.2.0_1", + :provider => :xbps, + }) + + expect(instances[1].properties).to eq({ + :name => "ruby-devel", + :ensure => "3.1.3_1", + :provider => :xbps, + }) + end + + it "should warn on invalid input" do + expect(described_class).to receive(:execpipe).and_yield(StringIO.new("blah")) + expect(described_class).to receive(:warning).with('Failed to match line \'blah\'') + expect(described_class.instances).to eq([]) + end + end + + describe "when installing" do + it "and install_options are given it should call xbps to install the package quietly with the passed options" do + @resource[:install_options] = ["-x", { "--arg" => "value" }] + args = ["-S", "-y", "-x", "--arg=value", @resource[:name]] + expect(@provider).to receive(:xbps_install).with(*args).and_return("") + expect(described_class).to receive(:execpipe).with(["/usr/bin/xbps-query", "-l"]) + + @provider.install + end + + it "and source is given it should call xbps to install the package from the source as repository" do + @resource[:source] = "/path/to/xbps/containing/directory" + args = ["-S", "-y", "--repository=#{@resource[:source]}", @resource[:name]] + expect(@provider).to receive(:xbps_install).at_least(:once).with(*args).and_return("") + expect(described_class).to receive(:execpipe).with(["/usr/bin/xbps-query", "-l"]) + + @provider.install + end + end + + describe "when updating" do + it "should call install" do + expect(@provider).to receive(:install).and_return("ran install") + expect(@provider.update).to eq("ran install") + end + end + + describe "when uninstalling" do + it "should call xbps to remove the right package quietly" do + args = ["-R", "-y", @resource[:name]] + expect(@provider).to receive(:xbps_remove).with(*args).and_return("") + @provider.uninstall + end + + it "adds any uninstall_options" do + @resource[:uninstall_options] = ["-x", { "--arg" => "value" }] + args = ["-R", "-y", "-x", "--arg=value", @resource[:name]] + expect(@provider).to receive(:xbps_remove).with(*args).and_return("") + @provider.uninstall + end + end + + describe "when determining the latest version" do + it "should return the latest version number of the package" do + @resource[:name] = "ruby-devel" + + expect(described_class).to receive(:execpipe).with(["/usr/bin/xbps-query", "-l"]).and_yield(StringIO.new(%{ +ii ruby-devel-3.1.3_1 Ruby programming language - development files +})) + + expect(@provider.latest).to eq("3.1.3_1") + end + end + + describe "when querying" do + it "should call self.instances and return nil if the package is missing" do + expect(described_class).to receive(:instances) + .and_return([]) + + expect(@provider.query).to be_nil + end + + it "should get real-package in case allow_virtual is true" do + @resource[:name] = "nodejs-runtime" + @resource[:allow_virtual] = true + + expect(described_class).to receive(:execpipe).with(["/usr/bin/xbps-query", "-l"]) + .and_yield(StringIO.new("")) + + args = ["-Rs", @resource[:name]] + expect(@provider).to receive(:xbps_query).with(*args).and_return(%{ +[*] nodejs-16.19.0_1 Evented I/O for V8 javascript +[-] nodejs-lts-12.22.10_2 Evented I/O for V8 javascript' +}) + + expect(@provider.query).to eq({ + :name => "nodejs", + :ensure => "16.19.0_1", + :provider => :xbps, + }) + end + end +end