Skip to content

Commit

Permalink
Merge pull request puppetlabs#8976 from Animeshz/xbps
Browse files Browse the repository at this point in the history
(maint) Add xbps used by voidlinux as a package provider
  • Loading branch information
joshcooper authored May 30, 2024
2 parents 2d96e71 + e4ed209 commit c260871
Show file tree
Hide file tree
Showing 2 changed files with 270 additions and 0 deletions.
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

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

0 comments on commit c260871

Please sign in to comment.