-
-
Save imobachgs/30942b4b89f4b33125ca9d1f6b1476b1 to your computer and use it in GitHub Desktop.
[RFC] Software management API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# # 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