Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(maint) Add xbps used by voidlinux as a package provider #8976

Merged
merged 1 commit into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions lib/puppet/provider/package/xbps.rb
Original file line number Diff line number Diff line change
@@ -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
joshcooper marked this conversation as resolved.
Show resolved Hide resolved

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
143 changes: 143 additions & 0 deletions spec/unit/provider/package/xbps_spec.rb
Original file line number Diff line number Diff line change
@@ -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