Skip to content

Commit

Permalink
Added new API for solving dependency problems
Browse files Browse the repository at this point in the history
- Support "N/A" libzypp repository type (originally "NONE")
5.0.3
  • Loading branch information
lslezak committed Jan 16, 2024
1 parent 8bbf2dc commit 56cd5ce
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
steps:

- name: Git Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Install Dependencies
run: rake build_dependencies:install
Expand Down
10 changes: 10 additions & 0 deletions package/yast2-pkg-bindings.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
-------------------------------------------------------------------
Tue Jan 16 14:32:15 UTC 2024 - Ladislav Slezák <[email protected]>

- Added new pkg calls for managing and resolving the solver
conflits. Added PkgSolveProblems(), PkgSetSolveSolutions()
and PkgResetSolveSolutions() calls. (gh#openSUSE/agama#944)
- Libzypp 17.31.26+ uses "N/A" for unknown repository types
instead of "NONE", support both cases (bsc#1218859)
- 5.0.3

-------------------------------------------------------------------
Wed Sep 20 15:54:13 UTC 2023 - Ladislav Slezák <[email protected]>

Expand Down
2 changes: 1 addition & 1 deletion package/yast2-pkg-bindings.spec
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@


Name: yast2-pkg-bindings
Version: 5.0.2
Version: 5.0.3
Release: 0
Summary: YaST2 - Package Manager Access
License: GPL-2.0-only
Expand Down
48 changes: 47 additions & 1 deletion smoke_test_run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def check_y2log
# no idea why that happens at Travis, let's ignore that...
y2log.reject! { |l| l =~ /error: Interrupted system call/ }

# ignore solver errors, we create them deliberately
y2log.reject! { |l| l =~ /Solverrun finished with an ERROR/ }
y2log.reject! { |l| l =~ /PkgSolve: [0-9]+ packages failed/ }

if !y2log.empty?
puts "Found errors in #{log_file}:"
puts y2log
Expand Down Expand Up @@ -150,7 +154,7 @@ def check_y2log

removed = Yast::Pkg.ResolvableRemove("yast2-core", :package)
raise "Cannot select yast2-core to uninstall" unless removed
puts "OK (yast2-core selected to uninstall"
puts "OK (yast2-core selected to uninstall)"

selected_packages = Yast::Pkg.Resolvables({kind: :package, status: :selected}, [:name])
raise "A package is selected to install (???)" unless selected_packages.empty?
Expand All @@ -161,6 +165,11 @@ def check_y2log
raise "yast2-core not selected to uninstall" unless removed_packages.include?({"name" => "yast2-core"})
puts "OK (yast2-core selected to uninstall)"

Yast::Pkg.ResolvableNeutral("yast2-core", :package, false) # false = no force
removed_packages = Yast::Pkg.Resolvables({kind: :package, status: :removed}, [:name])
raise "A package still selected to uninstall" if !removed_packages.empty?
puts "OK (yast2-core not selected to uninstall)"

installed_products = Yast::Pkg.Resolvables({kind: :product, status: :installed}, [:name, :display_name])
available_products = Yast::Pkg.Resolvables({kind: :product, status: :available}, [:name, :display_name])
selected_products = Yast::Pkg.Resolvables({kind: :product, status: :selected}, [:name, :display_name])
Expand All @@ -172,5 +181,42 @@ def check_y2log
puts "Found #{available_products.size} available products: #{available_products.map{|p| p["display_name"]}}"
puts "OK"

puts "Selecting opera package to install"
Yast::Pkg.ResolvableInstall("opera", :package)
solved = Yast::Pkg.PkgSolve(false)
# the OSS repository providing the X11 dependencies is disabled, the package
# cannot be installed without them
raise "A solver problem is expected" if solved

errors = Yast::Pkg.PkgSolveErrors
raise "One solver error is expected" if errors != 1
puts "OK (found one dependency error)"

problems = Yast::Pkg.PkgSolveProblems
raise "One solver problem is expected" if problems.size != 1
puts "OK (found one dependency problem)"

problem = problems.first
# select the "do not install" solution
solution = problem["solutions"].find{|s| s["description"].match(/do not install/i)}

puts "Selecting a conflict solution"
# set the selected solution
Yast::Pkg.PkgSetSolveSolutions(
[
{
"description" => problem["description"],
"details" => problem["details"],
"solution_description" => solution["description"],
"solution_details" => solution["details"]
}
]
)

# run the solver again
solved = Yast::Pkg.PkgSolve(false)
raise "A solver problem still exists" if !solved
puts "OK (no dependency error anymore)"

# scan y2log for errors
check_y2log
236 changes: 236 additions & 0 deletions src/Package.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2112,6 +2112,242 @@ PkgFunctions::PkgSolveErrors()
return YCPVoid();
}

/**
@builtin PkgSolveProblems
@short Returns details about the solver problems
Only valid after a call of PkgSolve/PkgSolveCheckTargetOnly that returned false,
otherwise the result might not correspond to the current state.
@note The texts in the result are translated and respect the current locale.
@return list of solver problems with suggested solutions
@example Returned data structure
[
{
# description of the problem
"description" => <string>,
# details of the problem, can be a multiline text, can an empty string
"details" => <string>,
# list of proposed solutions, only one of then can be applied
"solutions" =>
[
{
# description of the solution
"description" => <string>,
# details of the solution, can be a multiline text, can an empty string
"details" => <string>
}
]
}
]
@example Example data
[
{
"description" => "the to be installed yast2-theme-SLE-4.5.0-lp154.2.1.noarch " \
"conflicts with 'namespace:otherproviders(yast2-branding)' provided by the " \
"installed yast2-theme-5.0.1-lp154.2.1.noarch",
"details" => "",
"solutions" =>
[
{
"description" => "Following actions will be done:",
"details" =>
"deinstallation of yast2-theme-5.0.1-lp154.2.1.noarch\n" +
"deinstallation of yast2-theme-oxygen-5.0.1-lp154.2.1.noarch\n" +
"deinstallation of yast2-theme-breeze-5.0.1-lp154.2.1.noarch"
},
{
"description" => "do not install yast2-theme-SLE-4.5.0-lp154.2.1.noarch",
"details" => ""
}
]
}
]
*/
YCPValue
PkgFunctions::PkgSolveProblems()
{
YCPList ret;
auto problems = zypp_ptr()->resolver()->problems();
for_( problem, problems.begin(), problems.end() )
{
YCPMap problem_item;
// details about the problem
problem_item->add(YCPString("description"), YCPString((*problem)->description()));
problem_item->add(YCPString("details"), YCPString((*problem)->details()));

YCPList solutions;
for_( solution, (*problem)->solutions().begin(), (*problem)->solutions().end() )
{
YCPMap solution_item;
// details about the solution
solution_item->add(YCPString("description"), YCPString((*solution)->description()));
solution_item->add(YCPString("details"), YCPString((*solution)->details()));

solutions->add(solution_item);
}
problem_item->add(YCPString("solutions"), solutions);

ret->add(problem_item);
}
return ret;
}

/**
* @builtin PkgSetSolveSolutions
*
* Apply the proposed solver solutions. The input data is similar to the
* PkgSolveProblems() return value, just the solution data is a simple map,
* see the example.
*
* @see PkgSolveProblems
*
* @note When there are too many conflicts it might be better to apply the
* solutions iteratively in small batches instead of solving all problems at once.
* Some solutions might actually fix multiple problems.
*
* @note You need to run the solver after applying the solutions.
*
* @example Input data structure
* [
* {
* # description of the problem (from the PkgSolveProblems() data)
* "description" => <string>,
* # details of the problem (from the PkgSolveProblems() data)
* "details" => <string>,
* # description of the solution (from the PkgSolveProblems() data)
* "solution_description" => <string>,
* # details of the solution (from the PkgSolveProblems() data)
* "solution_details" => <string>
* }
* ]
*
* @example Example call
* # see the PkgSolveProblems() example
* PkgSetSolveSolutions([
* {
* "description" => "the to be installed yast2-theme-SLE-4.5.0-lp154.2.1.noarch " \
* "a word with 'namespace:otherproviders(yast2-branding)' provided by " \
* "the installed yast2-theme-5.0.1-lp154.2.1.noarch",
* "details" => "",
* "solution_description" => "Following actions will be done:",
* "solution_details" => "deinstallation of yast2-theme-5.0.1-lp154.2.1.noarch\n" \
* "deinstallation of yast2-theme-oxygen-5.0.1-lp154.2.1.noarch\n" \
* "deinstallation of yast2-theme-breeze-5.0.1-lp154.2.1.noarch"
* }
* ])
*
* @param solutions List of requested solutions
* @return YCPValue
*/
YCPValue
PkgFunctions::PkgSetSolveSolutions(const YCPList& solutions)
{
y2milestone("Requested %d solutions", solutions->size());

zypp::ProblemSolutionList user_solutions;
bool error = false;
for (int index = 0; index < solutions->size(); index++ )
{
if (solutions->value(index).isNull() || !solutions->value(index)->isMap())
{
y2error("Pkg::PkgSetSolveSolutions: entry not a map at index %d", index);
error = true;
continue;
}

YCPMap solution = solutions->value(index)->asMap();

// check missing keys
if (solution->value(YCPString("description")).isNull()
|| solution->value(YCPString("details")).isNull()
|| solution->value(YCPString("solution_description")).isNull()
|| solution->value(YCPString("solution_details")).isNull())
{
y2error("Pkg::PkgSetSolveSolutions: missing required map key at index %d", index);
error = true;
continue;
}

// check string keys
if (!solution->value(YCPString("description"))->isString()
|| !solution->value(YCPString("details"))->isString()
|| !solution->value(YCPString("solution_description"))->isString()
|| !solution->value(YCPString("solution_details"))->isString())
{
y2error("Pkg::PkgSetSolveSolutions: non-string map value at index %d", index);
error = true;
continue;
}

// get the keys
std::string problem_description = solution->value(YCPString("description"))->asString()->value();
std::string problem_details = solution->value(YCPString("details"))->asString()->value();
std::string solution_description = solution->value(YCPString("solution_description"))->asString()->value();
std::string solution_details = solution->value(YCPString("solution_details"))->asString()->value();

auto problems = zypp_ptr()->resolver()->problems();
bool found = false;
for_( problem, problems.begin(), problems.end() )
{
// find the problem
if ((*problem)->description() == problem_description && (*problem)->details() == problem_details)
{
for_( solution, (*problem)->solutions().begin(), (*problem)->solutions().end() )
{
// find the solution
if ((*solution)->description() == solution_description && (*solution)->details() == solution_details)
{
// remember the found solution
user_solutions.push_back(*solution);
found = true;
break;
}
}
break;
}
}

// the specified solution was not found
if (!found) error = true;
}

y2milestone("Applying %ld solutions", user_solutions.size());
zypp::getZYpp()->resolver()->applySolutions(user_solutions);

return YCPBoolean(!error);
}

/**
* @builtin PkgResetSolveSolutions
*
* Resets all applied solver solutions.
*
* @note You need to run the solver after applying the solutions.
*
* @note The changes done by the previous solutions are kept. It only resets
* the internal libzypp lists so the solutions are not applied again, but the
* already done changes are not reverted. E.g. when a solution was to uninstall
* a package then the packages is still selected to uninstall even after resetting
* the applied solutions.
*
* @note The applied solutions are done on the user level, technically it is
* possible to revert them manually if there are no other user changes.
*
* @param solutions List of applied solutions
*
* @return YCPVoid
*/
YCPValue
PkgFunctions::PkgResetSolveSolutions()
{
zypp::getZYpp()->resolver()->undo();
return YCPVoid();
}

namespace
{
///////////////////////////////////////////////////////////////////
Expand Down
6 changes: 6 additions & 0 deletions src/PkgFunctions.h
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,12 @@ class PkgFunctions
YCPBoolean PkgSolveCheckTargetOnly ();
/* TYPEINFO: integer()*/
YCPValue PkgSolveErrors ();
/* TYPEINFO: list<map<string,any>>()*/
YCPValue PkgSolveProblems ();
/* TYPEINFO: boolean(list<map<string,any>>)*/
YCPValue PkgSetSolveSolutions (const YCPList& solutions);
/* TYPEINFO: void()*/
YCPValue PkgResetSolveSolutions ();
YCPValue CommitHelper(const zypp::ZYppCommitPolicy *policy);
/* TYPEINFO: list<any>(integer)*/
YCPValue PkgCommit (const YCPInteger& medianr);
Expand Down
2 changes: 2 additions & 0 deletions src/Source_Misc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ std::string PkgFunctions::zypp2yastType(const std::string &type)
type_conversion_table["yast2"] = "YaST";
type_conversion_table["plaindir"] = "Plaindir";
type_conversion_table["NONE"] = "NONE";
// since libzypp-17.31.26
type_conversion_table["N/A"] = "NONE";
}

std::map<std::string,std::string>::const_iterator it = type_conversion_table.find(type);
Expand Down

0 comments on commit 56cd5ce

Please sign in to comment.