Skip to content

Commit

Permalink
dnf5: Implement "check" command using librpm
Browse files Browse the repository at this point in the history
Example output of the command "check":
pipewire-pulseaudio-0.3.84-3.fc39.x86_64
 missing require "libpipewire-0.3.so.0()(64bit)"
 missing require "pipewire-libs(x86-64) = 0.3.84-3.fc39"
 installed conflict "pulseaudio-daemon" from "pipewire-pulseaudio-0.3.85-1.fc39.x86_64"
 installed conflict "pulseaudio-daemon" from "pulseaudio-16.1-5.fc39.x86_64"
 duplicate with "pipewire-pulseaudio-0.3.85-1.fc39.x86_64"
  • Loading branch information
jrohel committed Nov 29, 2023
1 parent 763f9cc commit 33840a1
Show file tree
Hide file tree
Showing 5 changed files with 402 additions and 0 deletions.
1 change: 1 addition & 0 deletions dnf5.spec
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Provides: dnf5-command(reinstall)
Provides: dnf5-command(swap)
Provides: dnf5-command(mark)
Provides: dnf5-command(autoremove)
Provides: dnf5-command(check)
Provides: dnf5-command(check-upgrade)

Provides: dnf5-command(leaves)
Expand Down
3 changes: 3 additions & 0 deletions dnf5/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ set_property(TARGET dnf5 PROPERTY ENABLE_EXPORTS 1)
target_link_libraries(dnf5 PRIVATE common libdnf5 libdnf5-cli Threads::Threads)
install(TARGETS dnf5 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

pkg_check_modules(RPM REQUIRED rpm>=4.17.0)
target_link_libraries(dnf5 PRIVATE ${RPM_LIBRARIES})


find_package(bash-completion)
if(BASH_COMPLETION_FOUND)
Expand Down
357 changes: 357 additions & 0 deletions dnf5/commands/check/check.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
/*
Copyright Contributors to the libdnf project.
This file is part of libdnf: https://github.com/rpm-software-management/libdnf/
Libdnf is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Libdnf is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with libdnf. If not, see <https://www.gnu.org/licenses/>.
*/

#include "check.hpp"

#include <fmt/format.h>
#include <libdnf5/rpm/nevra.hpp>
#include <libdnf5/utils/bgettext/bgettext-lib.h>
#include <libdnf5/utils/bgettext/bgettext-mark-domain.h>
#include <rpm/header.h>
#include <rpm/rpmcli.h>
#include <rpm/rpmps.h>
#include <rpm/rpmtag.h>
#include <rpm/rpmtd.h>
#include <rpm/rpmte.h>
#include <rpm/rpmts.h>

#include <iostream>
#include <map>
#include <set>
#include <string>
#include <type_traits>
#include <vector>

namespace dnf5 {

using namespace libdnf5;

namespace {

enum class ProblemType { MISSING_REQUIRE, CONFLICT, OBSOLETED, DUPLICATE };

// Package is identified by "nevra". But there may be a situation where there are several packages with
// the same "nevra" in the system (broken installation?).
// That's why we also keep a unique package index from the rpm database.
struct PkgId {
std::string nevra;
std::invoke_result_t<decltype(&rpmteDBOffset), rpmte> rpmdb_idx;

bool operator==(const PkgId & p) const noexcept { return rpmdb_idx == p.rpmdb_idx; }

std::strong_ordering operator<=>(const PkgId & p) const noexcept {
auto cmp = nevra <=> p.nevra;
return cmp != 0 ? cmp : rpmdb_idx <=> p.rpmdb_idx;
}
};

// Describes one problem
struct Problem {
ProblemType type; // type of problem
std::string nevra; // "nevra" causing this problem
std::string file_or_provide; // "file" or "provide" causing this problem

std::strong_ordering operator<=>(const Problem & p) const noexcept {
if (auto cmp = type <=> p.type; cmp != 0) {
return cmp;
}
if (auto cmp = file_or_provide <=> p.file_or_provide; cmp != 0) {
return cmp;
}
return nevra <=> p.nevra;
}
};

// what the program will check
bool all{true};
bool dependencies{false};
bool duplicates{false};
bool obsoleted{false};

// Contains "provides" to recognize the installonly package.
// It is initialized with values from the configuration.
std::vector<std::string> installonly_pkgs_provides;

// Contains a set of problems for each problematic package.
// The std::map Key is the "nevra" of the problematic package.
std::map<PkgId, std::set<Problem>> problems;

// Contains a list of installed packages for each "name.arch".
// For "installonly" packages, only one package is present.
// Usually contains 1 package for "name.arch". Multiple packages means "duplicates".
std::map<std::string, std::vector<PkgId>> installed_na_packages;


cli::ArgumentParser::NamedArg * create_option(
cli::ArgumentParser & parser, const std::string & name, const std::string & descr, bool & opt_to_modify) {
auto option = parser.add_new_named_arg(name);
option->set_long_name(name);
option->set_description(descr);
option->set_parse_hook_func([&opt_to_modify](cli::ArgumentParser::NamedArg *, const char *, const char *) {
opt_to_modify = true;
return true;
});
return option;
}


inline bool is_package_installonly(Header h) noexcept {
rpmtd td = rpmtdNew();
headerGet(h, RPMTAG_PROVIDES, td, 0);
while (const auto * const provide = rpmtdNextString(td)) {
for (const auto & installonly_pkg_provide : installonly_pkgs_provides) {
if (installonly_pkg_provide == provide) {
rpmtdFree(td);
return true;
}
}
}
rpmtdFree(td);

return false;
}


// Ensures that NEVRA always contains an EPOCH.
// Returns the input `nevra`. EPOCH is inserted if it was not on the input.
// librpm functions such as "rpmProblemGetPkgNEVR()" and "rpmProblemGetAltNEVR()" return epoch 0 only sometimes.
// We want the epoch always.
inline std::string ensure_full_nevra(const char * nevra) {
const auto nevras = rpm::Nevra::parse(nevra, {rpm::Nevra::Form::NEVRA});
return rpm::to_full_nevra_string(nevras.front());
}


// Finds problems for the installed package defined by Header h.
// Principle: Adds the installed package to an empty transaction and checks for errors.
int find_problems(QVA_t qva, rpmts ts, Header h) {
int rc = 0;

if (qva->qva_flags & VERIFY_DEPS) {
rpmtsEmpty(ts);
(void)rpmtsAddInstallElement(ts, h, NULL, 0, NULL);

(void)rpmtsCheck(ts);
auto te = rpmtsElement(ts, 0);
auto ps = rpmteProblems(te);
rc = rpmpsNumProblems(ps);

if (rc > 0) {
auto rpmdb_idx = rpmteDBOffset(te);
rpmpsi psi = rpmpsInitIterator(ps);
while (auto p = rpmpsiNext(psi)) {
const auto * const problem_cstr = rpmProblemGetStr(p);
libdnf_assert(problem_cstr != NULL, "command \"check\": rpmProblemGetStr() returns NULL");
const auto * const cnevra = rpmProblemGetPkgNEVR(p);
libdnf_assert(cnevra != NULL, "command \"check\": rpmProblemGetPkgNEVR() returns NULL");
const auto * const alt_cnevra = rpmProblemGetAltNEVR(p);
libdnf_assert(alt_cnevra != NULL, "command \"check\": rpmProblemGetAltNEVR() returns NULL");

switch (rpmProblemGetType(p)) {
case RPMPROB_REQUIRES:
if (all || dependencies) {
problems[{ensure_full_nevra(alt_cnevra), rpmdb_idx}].insert(Problem{
.type = ProblemType::MISSING_REQUIRE, .nevra = "", .file_or_provide = problem_cstr});
}
break;
case RPMPROB_CONFLICT:
if (all || dependencies) {
auto nevra = ensure_full_nevra(cnevra);
auto alt_nevra = ensure_full_nevra(alt_cnevra);
if (nevra == alt_nevra) {
// skip self conflicts
break;
}
problems[{nevra, rpmdb_idx}].insert(Problem{
.type = ProblemType::CONFLICT, .nevra = alt_nevra, .file_or_provide = problem_cstr});
}
break;
case RPMPROB_OBSOLETES:
if (all || obsoleted) {
auto nevra = ensure_full_nevra(cnevra);
auto alt_nevra = ensure_full_nevra(alt_cnevra);
if (nevra == alt_nevra) {
// skip self obsolete
break;
}
problems[{nevra, rpmdb_idx}].insert(Problem{
.type = ProblemType::OBSOLETED, .nevra = alt_nevra, .file_or_provide = problem_cstr});
}
break;
default:;
}
}
rpmpsFreeIterator(psi);
}
rpmpsFree(ps);
rpmtsEmpty(ts);
}
return rc;
}


// Groups packages with the same "name" and "architecture".
// For "installonly" packages, only one "nevra" is inserted.
int group_pkgs_same_name_arch([[maybe_unused]] QVA_t qva, [[maybe_unused]] rpmts ts, Header h) {
const auto * const arch = headerGetString(h, RPMTAG_ARCH);
if (!arch) {
// skip any non-packages (such as gpg-pubkey) in the database
return 0;
}

const auto * const name = headerGetString(h, RPMTAG_NAME);

auto & nevras = installed_na_packages[fmt::format("{}.{}", name, arch)];
if (nevras.empty() || !is_package_installonly(h)) {
// "duplicate" packages are only added if they are not "installonly" packages
// to avoid being reported as duplicates.

// "headerGetAsString(h, RPMTAG_NEVRA)" returns epoch 0 only sometimes.
// We want the epoch always. We build NEVRA ourselves.
const auto epoch = headerGetNumber(h, RPMTAG_EPOCH);
const auto * const version = headerGetString(h, RPMTAG_VERSION);
const auto * const release = headerGetString(h, RPMTAG_RELEASE);
auto rpmdb_idx = static_cast<decltype(PkgId::rpmdb_idx)>(headerGetInstance(h));
nevras.push_back({fmt::format("{}-{}:{}-{}.{}", name, epoch, version, release, arch), rpmdb_idx});
}

return 0;
}

} // namespace


void CheckCommand::set_parent_command() {
auto * arg_parser_parent_cmd = get_session().get_argument_parser().get_root_command();
auto * arg_parser_this_cmd = get_argument_parser_command();
arg_parser_parent_cmd->register_command(arg_parser_this_cmd);
arg_parser_parent_cmd->get_group("software_management_commands").register_argument(arg_parser_this_cmd);
}


void CheckCommand::set_argument_parser() {
auto & ctx = get_context();
auto & parser = ctx.get_argument_parser();

auto & cmd = *get_argument_parser_command();
cmd.set_description("Check for problems in the packagedb");
cmd.set_long_description(
"Checks the local packagedb and produces information on any problems it finds.\n"
"The set of checks performed can be specified with options.");

cmd.register_named_arg(
create_option(parser, "dependencies", "Show missing dependencies and conflicts", dependencies));
cmd.register_named_arg(create_option(parser, "duplicates", "Show duplicated packages", duplicates));
cmd.register_named_arg(create_option(parser, "obsoleted", "Show obsoleted packages", obsoleted));
}


void CheckCommand::configure() {
installonly_pkgs_provides = get_context().base.get_config().get_installonlypkgs_option().get_value();
all = !(dependencies || duplicates || obsoleted);
}


void CheckCommand::run() {
auto & ctx = get_context();

auto ts = rpmtsCreate();
auto & config = ctx.base.get_config();
rpmtsSetRootDir(ts, config.get_installroot_option().get_value().c_str());

if (all || dependencies || obsoleted) {
QVA_t qva = &rpmQVKArgs;
qva->qva_mode = 'V';
qva->qva_flags = VERIFY_DEPS;
qva->qva_source = RPMQV_ALL;
qva->qva_sourceCount = 1;
qva->qva_showPackage = find_problems;

struct rpmInstallArguments_s * ia = &rpmIArgs;
rpmtsSetFlags(ts, rpmtsFlags(ts) | (ia->transFlags & RPMTRANS_FLAG_NOPLUGINS));

rpmcliVerify(ts, qva, NULL);
}

if (all || duplicates) {
QVA_t qva = &rpmQVKArgs;
qva->qva_mode = 'q';
qva->qva_flags = VERIFY_DEPS;
qva->qva_source = RPMQV_ALL;
qva->qva_sourceCount = 1;
qva->qva_showPackage = group_pkgs_same_name_arch;

struct rpmInstallArguments_s * ia = &rpmIArgs;
rpmtsSetFlags(ts, rpmtsFlags(ts) | (ia->transFlags & RPMTRANS_FLAG_NOPLUGINS));

rpmcliQuery(ts, qva, NULL);

for (const auto & [na, packages] : installed_na_packages) {
if (packages.size() > 1) {
for (const auto & package : packages) {
for (const auto & duplicate_package : packages) {
if (package == duplicate_package) {
// skip itself
continue;
}
problems[package].insert(Problem{
.type = ProblemType::DUPLICATE, .nevra = duplicate_package.nevra, .file_or_provide = ""});
}
}
}
}
}

rpmtsFree(ts);

// Print problems
if (!problems.empty()) {
std::size_t problem_count{0};
for (const auto & [package_id, problems_list] : problems) {
std::cout << package_id.nevra << std::endl;
for (const auto & problem : problems_list) {
++problem_count;
switch (problem.type) {
case ProblemType::MISSING_REQUIRE:
std::cout << fmt::format(" missing require \"{}\"", problem.file_or_provide) << std::endl;
break;
case ProblemType::CONFLICT:
std::cout << fmt::format(
" installed conflict \"{}\" from \"{}\"",
problem.file_or_provide,
problem.nevra)
<< std::endl;
break;
case ProblemType::OBSOLETED:
std::cout << fmt::format(
" obsoleted by \"{}\" from \"{}\"", problem.file_or_provide, problem.nevra)
<< std::endl;
break;
case ProblemType::DUPLICATE:
std::cout << fmt::format(" duplicate with \"{}\"", problem.nevra) << std::endl;
break;
}
}
}

throw Error(M_("Check discovered {} problem(s) in {} package(s)"), problem_count, problems.size());
}
}

} // namespace dnf5
Loading

0 comments on commit 33840a1

Please sign in to comment.