Skip to content

Instantly share code, notes, and snippets.

@imobachgs
Last active October 28, 2021 11:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save imobachgs/30942b4b89f4b33125ca9d1f6b1476b1 to your computer and use it in GitHub Desktop.
Save imobachgs/30942b4b89f4b33125ca9d1f6b1476b1 to your computer and use it in GitHub Desktop.
[RFC] Software management API
# # Defining a new software management API
#
# This file contains a tentative API for software management. It is still a WIP, but
# it serves as an indication of the direction we are following.
#
# * Define an class that acts as entry point (like StorageManager). Check the {SoftwareManager} class.
# * Support for multiple backends. In the future we could add support for DNF or event Flatpak.
# * Classes like `Package` or `Product` should be backend-agnostic.
# * Unlike storage-ng or network-ng, it is not possible to keep all the software information
# in Ruby classes (it will simply consume *a lot* of memory). So the idea is to only keep
# references to relevant things.
# * Offer a rich query API (see {SoftwareQuery}.
require "yast"
require "yast2/execute"
Yast.import "Arch"
# @example Initialize the software subsystem with libzypp and flatpak backends
# software = SoftwareManager.new
# software.add_backend(LibzyppBackend.new)
# software.add_backend(FlatpakBackend.new)
# software.init
#
# @example Listing all repositories, including libzypp and flatpak
# repos = software.repositories.each do |repo|
# puts "#{repo.class}: #{repo.id}\t#{repo.name}\t#{repo.url}"
# end
#
# @example Searching packages/applications through all the backends
# query = SoftwareQuery.new(software.backends)
# .with_term("yast2")
# query.to_a.each do |res|
# puts "#{res.name}"
# end
#
# @example Convenience method to query all backends
# query = software.query.with_term("yast2").with_provides("release-notes()")
# query.to_a do |resolvable|
# puts resolvable.name
# end
#
# @example Installing a package on the fly. We might need to tell which backend to use.
# software.install("yast2", :package)
#
# @example Installing an application on the fly. We might need to tell which backend to use.
# software.install("Discord", :application)
#
# @example Alternatively, we could provide a mechanism to install resolvables from their own
# instances
#
# pkg = software.find("yast2", :package)
# pkg.install # the resolvable should contain a reference to the backend
#
# @example Create a software proposal
# proposal = SoftwareProposal.new
# proposal.add_package("yast2") # or pass Package.search("yast2").first
# proposal.product = 'SLES' # or Product.find("SLES")
# software.commit(proposal)
require "byebug"
# Represents software backends
#
# They implement the mechanism to initialize the backend, search for resolvables,
# install them, etc.
class Backend
# Initializes the backend
def probe
end
# Returns the list of repositories
#
# @return [Array<Repository>]
def repositories
[]
end
# Searches for packages according to the backend
#
# @todo Define a better (and more generic) API. Perpahs it should receive a SoftwareQuery instance.
# @todo What about returning a SoftwareQueryResult object? It may contain facilities to filter
# the result, order them, and so on.
#
# @return [Array<Resolvable>] List of resolvables.
def search(term:, provides: [])
[]
end
end
# libzypp backend
class LibzyppBackend < Backend
# Initialize the libzypp subsystem using the pkg-bindings
def probe
Yast.import 'Pkg'
Yast.import 'PackageLock'
Yast::Pkg.TargetInitialize('/')
Yast::Pkg.TargetLoad
Yast::Pkg.SourceRestore
Yast::Pkg.SourceLoad
end
# Reads the repositories from the system
#
# @return [Array<RpmRepo>]
def repositories
Yast::Pkg.SourceGetCurrent(false).map do |repo_id|
repo_data = Yast::Pkg.SourceGeneralData(repo_id)
raise NotFound if repo_data.nil?
RpmRepo.new(repo_id, repo_data["name"], repo_data["raw_url"])
end
end
def search(term:, provides: [])
Yast::Pkg.ResolvableProperties(term, :package, "").map do |pkg|
Package.new(pkg["name"], pkg["version"], pkg["arch"], pkg["source"])
end
end
end
# Flatpak backend
class FlatpakBackend < Backend
# Reads the repositories from the system
#
# @return [Array<FlatpakRepo>]
def repositories
output = Yast::Execute.locally(
["flatpak", "remotes", "--system", "--columns", "name,title,url"],
stdout: :capture
)
output.lines.map do |line|
id, title, url = line.split(" ")
FlatpakRepo.new(id, title, url)
end
end
def search(term:, provides: [])
output = Yast::Execute.locally(
["flatpak", "search", "--system", "--columns", "name,version", term],
stdout: :capture
)
output.lines.map do |line|
name, version = line.split(" ")
Application.new(name, version, Yast::Arch.architecture, nil)
end
end
end
# Represents a repository (a RPM, flatpak... repository).
class Repository
attr_reader :id, :url, :name
def initialize(id, name, url)
@id = id
@name = name
@url = url
end
end
# @todo Do we need a separate class for this?
class RpmRepo < Origin
end
# @todo Do we need a separate class for this?
class FlatpakRepo < Origin
end
# Represents an element from the software system (a package, a product, an application, and so on)
#
# @todo The resolvable may take a reference to the backend it belongs, so it might be possible
# to implement {#install}, {#select} and {#remove} methods.
class Resolvable
attr_reader :name, :version, :arch, :repo_id
def initialize(name, version, arch, repo_id = nil)
@name = name
@version = version
@arch = arch
@repo_id = repo_id
end
end
# A package from a typical Linux packaging system
class Package < Resolvable
end
# A flatpak/snap/whatever application
class Application < Resolvable
end
class Product < Resolvable # should it inherit from the package?
end
# Represents a search for packages, products and applications
#
# @note This is just a sketch, as this class could evolve to something as complex as we want.
class SoftwareQuery
attr_reader :backends, :term, :provides
def initialize(backends)#, order: order)
@backends = backends # it could receive the backends instead of the whole SoftwareManager class.
@provides = []
end
def with_term(text)
@term = text
self
end
def with_provides(provide)
@provides << provide
self
end
def to_a
resolvables = backends.each_with_object([]) do |backend, all|
all.concat(backend.search(term: term, provides: provides))
end
ResolvablesCollection.new(resolvables)
end
end
# This class is basically a wrapper around an array of Resolvable objects. It can be used
# to extend the Array class in this case (filtering, sorting, etc.) at client side.
class ResolvablesCollection
attr_reader :resolvables
def initialize(*resolvables)
@resolvables = resolvables
end
def to_a
resolvables
end
end
# This class represents the list of resolvalbes to install/remove.
#
# It replaces modules like [PackagesAI](https://github.com/yast/yast-yast2/blob/master/library/packages/src/modules/PackageAI.rb)
# which keeps a list of stuff to install.
class SoftwareProposal
def initialize
@resolvables = []
end
def add_resolvable(resolvable)
@resolvables << resolvable
end
end
# Represents the software management subsystem itself.
#
# @example Initialize the software subsystem with the libzypp backend
# software = SoftwareManager.new
# software.add_backend(LibzyppBackend.new)
# software.init
#
class SoftwareManager
attr_reader :packages # Array<Array<Package, WantedState>>
attr_reader :backends
def initialize
@backends = []
end
# Initializes the software subsystem
def probe
backends.each(&:init)
end
# @param [SoftwareProposal]
def commit(proposal)
# ask the backends to install the given packages/apps
end
# Add a backend
#
# @todo We could specify the list of backends in the constructor.
def add_backend(backend)
@backends << backend
end
# Method to get the list of repositories from all the backends
#
# @return [Array<Origin>] Defined repositories
def repositories
backends.each_with_object([]) do |backend, all|
all.concat(backend.repositories)
end
end
# Returns a query object for all backends
#
# @return [SoftwareQuery]
def query
SoftwareQuery.new(backends)
end
end
software = SoftwareManager.new
software.add_backend(LibzyppBackend.new)
software.add_backend(FlatpakBackend.new)
software.probe
software.repositories.each do |repo|
puts "#{repo.class}: #{repo.id}\t#{repo.name}\t#{repo.url}"
end
software.query.with_term("gnome").to_a.each do |pkg|
puts "Name: #{pkg.name}\tVersion: #{pkg.version}\tArch: #{pkg.arch}\tRepo: #{pkg.repo_id}"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment