diff --git a/bindings/libdnf5/rpm.i b/bindings/libdnf5/rpm.i index 3ed28752a..57d11716b 100644 --- a/bindings/libdnf5/rpm.i +++ b/bindings/libdnf5/rpm.i @@ -66,6 +66,9 @@ %include "libdnf/rpm/reldep_list.hpp" %include "libdnf/rpm/package.hpp" +%template(VectorPackage) std::vector; +%template(VectorVectorPackage) std::vector>; + %template(VectorChangelog) std::vector; %rename(next) libdnf::rpm::PackageSetIterator::operator++(); diff --git a/dnf5.spec b/dnf5.spec index af5769688..06cc69fbc 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -211,6 +211,7 @@ It supports RPM packages, modulemd modules, and comps groups & environments. # TODO(jkolarik): history is not ready yet # %%{_mandir}/man8/dnf5-history.8.* %{_mandir}/man8/dnf5-install.8.* +%{_mandir}/man8/dnf5-leaves.8.* %{_mandir}/man8/dnf5-makecache.8.* %{_mandir}/man8/dnf5-mark.8.* # TODO(jkolarik): module is not ready yet diff --git a/dnf5/commands/leaves/leaves.cpp b/dnf5/commands/leaves/leaves.cpp new file mode 100644 index 000000000..1fda7bd51 --- /dev/null +++ b/dnf5/commands/leaves/leaves.cpp @@ -0,0 +1,88 @@ +/* +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 . +*/ + +#include "leaves.hpp" + +#include +#include +#include + +#include + +namespace dnf5 { + +using namespace libdnf::cli; + +void LeavesCommand::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("query_commands").register_argument(arg_parser_this_cmd); +} + +void LeavesCommand::set_argument_parser() { + get_argument_parser_command()->set_description( + "List groups of installed packages not required by other installed packages"); + get_argument_parser_command()->set_long_description( + R"(The `leaves` command is used to list all leaf packages. + +Leaf packages are installed packages that are not required as a dependency +of another installed package. However, two or more installed packages might +depend on each other in a dependency cycle. Packages in such cycles that +are not required by any other installed package are also leaf. +Packages in such cycles form a group of leaf packages. + +Packages in the output list are sorted by group and the first package +in the group is preceded by a '-' character.)"); +} + +void LeavesCommand::configure() { + auto & context = get_context(); + context.set_load_system_repo(true); + context.set_load_available_repos(Context::LoadAvailableRepos::NONE); +} + +void LeavesCommand::run() { + auto & ctx = get_context(); + + libdnf::rpm::PackageQuery leaves_package_query(ctx.base); + auto leaves_package_groups = leaves_package_query.filter_leaves_groups(); + + for (auto & package_group : leaves_package_groups) { + std::sort(package_group.begin(), package_group.end()); + } + std::sort( + leaves_package_groups.begin(), + leaves_package_groups.end(), + [](const std::vector & a, const std::vector & b) { + return a[0] < b[0]; + }); + + // print the packages grouped by their components + for (const auto & package_group : leaves_package_groups) { + char mark = '-'; + + for (const auto & package : package_group) { + std::cout << mark << ' ' << package.get_full_nevra() << '\n'; + mark = ' '; + } + } +} + +} // namespace dnf5 diff --git a/dnf5/commands/leaves/leaves.hpp b/dnf5/commands/leaves/leaves.hpp new file mode 100644 index 000000000..94f647ed4 --- /dev/null +++ b/dnf5/commands/leaves/leaves.hpp @@ -0,0 +1,41 @@ +/* +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 . +*/ + +#ifndef DNF5_COMMANDS_LEAVES_LEAVES_HPP +#define DNF5_COMMANDS_LEAVES_LEAVES_HPP + +#include + +#include +#include + +namespace dnf5 { + +class LeavesCommand : public Command { +public: + explicit LeavesCommand(Context & context) : Command(context, "leaves") {} + void set_parent_command() override; + void set_argument_parser() override; + void configure() override; + void run() override; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_LEAVES_LEAVES_HPP diff --git a/dnf5/commands/repoquery/repoquery.cpp b/dnf5/commands/repoquery/repoquery.cpp index 8f61c1e5c..ce09730ab 100644 --- a/dnf5/commands/repoquery/repoquery.cpp +++ b/dnf5/commands/repoquery/repoquery.cpp @@ -54,6 +54,9 @@ void RepoqueryCommand::set_argument_parser() { installed_option = dynamic_cast( parser.add_init_value(std::unique_ptr(new libdnf::OptionBool(false)))); + leaves_option = dynamic_cast( + parser.add_init_value(std::unique_ptr(new libdnf::OptionBool(false)))); + info_option = dynamic_cast( parser.add_init_value(std::unique_ptr(new libdnf::OptionBool(false)))); @@ -76,6 +79,13 @@ void RepoqueryCommand::set_argument_parser() { installed->set_const_value("true"); installed->link_value(installed_option); + auto leaves = parser.add_new_named_arg("leaves"); + leaves->set_long_name("leaves"); + leaves->set_description("Limit to groups of installed packages not required by other installed packages."); + leaves->set_const_value("true"); + leaves->link_value(leaves_option); + leaves->add_conflict_argument(*available); + auto info = parser.add_new_named_arg("info"); info->set_long_name("info"); info->set_description("Show detailed information about the packages."); @@ -269,6 +279,7 @@ void RepoqueryCommand::set_argument_parser() { cmd.register_named_arg(available); cmd.register_named_arg(installed); + cmd.register_named_arg(leaves); cmd.register_named_arg(latest_limit); cmd.register_named_arg(whatdepends); @@ -288,7 +299,7 @@ void RepoqueryCommand::set_argument_parser() { void RepoqueryCommand::configure() { auto & context = get_context(); context.update_repo_metadata_from_specs(pkg_specs); - only_system_repo_needed = installed_option->get_value() || duplicates->get_value(); + only_system_repo_needed = installed_option->get_value() || duplicates->get_value() || leaves_option->get_value(); context.set_load_system_repo(only_system_repo_needed); bool updateinfo_needed = advisory_name->get_value().empty() || advisory_security->get_value() || advisory_bugfix->get_value() || advisory_enhancement->get_value() || @@ -334,6 +345,10 @@ void RepoqueryCommand::run() { libdnf::rpm::PackageSet result_pset(ctx.base); libdnf::rpm::PackageQuery full_package_query(ctx.base); + if (leaves_option->get_value()) { + full_package_query.filter_leaves(); + } + auto advisories = advisory_query_from_cli_input( ctx.base, advisory_name->get_value(), diff --git a/dnf5/commands/repoquery/repoquery.hpp b/dnf5/commands/repoquery/repoquery.hpp index f4d6256f6..2f164897b 100644 --- a/dnf5/commands/repoquery/repoquery.hpp +++ b/dnf5/commands/repoquery/repoquery.hpp @@ -47,6 +47,7 @@ class RepoqueryCommand : public Command { libdnf::OptionBool * available_option{nullptr}; libdnf::OptionBool * installed_option{nullptr}; + libdnf::OptionBool * leaves_option{nullptr}; libdnf::OptionBool * info_option{nullptr}; libdnf::OptionNumber * latest_limit_option{nullptr}; std::vector pkg_specs; diff --git a/dnf5/main.cpp b/dnf5/main.cpp index d713387f9..eed10ef19 100644 --- a/dnf5/main.cpp +++ b/dnf5/main.cpp @@ -27,6 +27,7 @@ along with libdnf. If not, see . #include "commands/group/group.hpp" #include "commands/history/history.hpp" #include "commands/install/install.hpp" +#include "commands/leaves/leaves.hpp" #include "commands/list/info.hpp" #include "commands/list/list.hpp" #include "commands/makecache/makecache.hpp" @@ -485,6 +486,7 @@ static void add_commands(Context & context) { context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); + context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 14ff388cc..2eb45e11c 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -58,6 +58,7 @@ if(WITH_MAN) # TODO(jkolarik): history is not ready yet # install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-history.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-install.8 DESTINATION share/man/man8) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-leaves.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-makecache.8 DESTINATION share/man/man8) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/man/dnf5-mark.8 DESTINATION share/man/man8) # TODO(jkolarik): module is not ready yet diff --git a/doc/commands/index.rst b/doc/commands/index.rst index aab56c48b..d512d4603 100644 --- a/doc/commands/index.rst +++ b/doc/commands/index.rst @@ -14,6 +14,7 @@ DNF5 Commands environment.8 group.8 install.8 + leaves.8 makecache.8 mark.8 reinstall.8 diff --git a/doc/commands/leaves.8.rst b/doc/commands/leaves.8.rst new file mode 100644 index 000000000..fefe4b879 --- /dev/null +++ b/doc/commands/leaves.8.rst @@ -0,0 +1,58 @@ +.. + 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 . + +.. _leaves_command_ref-label: + +################ + Leaves Command +################ + +Synopsis +======== + +``dnf5 leaves`` + + +Description +=========== + +The ``leaves`` command in ``DNF5`` is used to list all leaf packages. +Leaf packages are installed packages that are not required as a dependency of another installed package. +However, two or more installed packages might depend on each other in a dependency cycle. Packages +in such cycles that are not required by any other installed package are also leaf. +Packages in such cycles form a group of leaf packages. + +Packages in the output list are sorted by group and the first package in the group is preceded by a ``-`` character. + + +Options +======= + +Does not implement options. But it takes into account the ``install_weak_deps`` setting. +If ``install_weak_deps`` is set to ``false``, weak dependencies are ignored during the calculation of the set of leaf packages. + + +Why is this useful? +=================== + +The list gives you a nice overview of what is installed on your system without flooding you with anything required by the packages already shown. +The following list of arguments basically says the same thing in different ways: + +* All the packages on this list is either needed by you, other users of the system or not needed at all -- if it was required by another installed package it would not be on the list. +* If you want to uninstall anything from your system (without breaking dependencies) it must involve at least one package on this list. +* If there is anything installed on the system which is not needed it must be on this list -- otherwise it would be required as a dependency by another package. diff --git a/doc/commands/repoquery.8.rst b/doc/commands/repoquery.8.rst index a922703a3..ab3ea8801 100644 --- a/doc/commands/repoquery.8.rst +++ b/doc/commands/repoquery.8.rst @@ -78,6 +78,9 @@ Options ``--installed`` | Display only installed packages. +``--leaves`` + | Display only leaf packages. + ``--nevra`` | Use the `NEVRA` format for the output. | This is the default behavior. @@ -104,4 +107,5 @@ See Also ======== | :manpage:`dnf5-advisory(8)`, :ref:`Advisory command ` + | :manpage:`dnf5-leaves(8)`, :ref:`Leaves command ` | :manpage:`dnf5-specs(7)`, :ref:`Patterns specification ` diff --git a/doc/conf.py.in b/doc/conf.py.in index d635365db..2e387a93d 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -115,6 +115,7 @@ man_pages = [ ('commands/group.8', 'dnf5-group', 'Group Command', AUTHORS, 8), ('commands/history.8', 'dnf5-history', 'History Command', AUTHORS, 8), ('commands/install.8', 'dnf5-install', 'Install Command', AUTHORS, 8), + ('commands/leaves.8', 'dnf5-leaves', 'Leaves Command', AUTHORS, 8), ('commands/makecache.8', 'dnf5-makecache', 'Makecache Command', AUTHORS, 8), ('commands/mark.8', 'dnf5-mark', ' Mark Command', AUTHORS, 8), ('commands/module.8', 'dnf5-module', ' Module Command', AUTHORS, 8), diff --git a/doc/dnf5.8.rst b/doc/dnf5.8.rst index 1526f7550..e1e8ffad5 100644 --- a/doc/dnf5.8.rst +++ b/doc/dnf5.8.rst @@ -71,6 +71,9 @@ For more details see the separate man page for the specific command, f.e. ``man :ref:`install ` | Install packages. +:ref:`leaves ` + | List groups of leaf packages. + :ref:`makecache ` | Generate the metadata cache. @@ -294,6 +297,7 @@ Commands in detail: | :manpage:`dnf5-environment(8)`, :ref:`Environment command ` | :manpage:`dnf5-group(8)`, :ref:`Group command ` | :manpage:`dnf5-install(8)`, :ref:`Install command ` + | :manpage:`dnf5-leaves(8)`, :ref:`Leaves command ` | :manpage:`dnf5-makecache(8)`, :ref:`Makecache command ` | :manpage:`dnf5-mark(8)`, :ref:`Mark command ` | :manpage:`dnf5-reinstall(8)`, :ref:`Reinstall command ` diff --git a/include/libdnf/rpm/package_query.hpp b/include/libdnf/rpm/package_query.hpp index b9b3cd180..848becdcf 100644 --- a/include/libdnf/rpm/package_query.hpp +++ b/include/libdnf/rpm/package_query.hpp @@ -638,7 +638,26 @@ class PackageQuery : public PackageSet { /// Filter packages to keep only duplicates of installed packages. Packages are duplicate if they have the same `name` and `arch` but different `evr`. void filter_duplicates(); + /// Filter the leaf packages. + /// + /// Leaf packages are installed packages that are not required as a dependency of another installed package. + /// However, two or more installed packages might depend on each other in a dependency cycle. Packages + /// in such cycles that are not required by any other installed package are also leaf. + void filter_leaves(); + + /// Filter the leaf packages and return them grouped by their dependencies. + /// + /// Leaf packages are installed packages that are not required as a dependency of another installed package. + /// However, two or more installed packages might depend on each other in a dependency cycle. Packages + /// in such cycles that are not required by any other installed package are also leaf. + /// Packages in such cycles form a group of leaf packages. + /// + /// @return Groups of one or more interdependent leaf packages. + std::vector> filter_leaves_groups(); + private: + std::vector> filter_leaves(bool return_grouped_leaves); + friend libdnf::Goal; class PQImpl; std::unique_ptr p_pq_impl; diff --git a/libdnf/rpm/package_query.cpp b/libdnf/rpm/package_query.cpp index 57fe23802..afd0d992c 100644 --- a/libdnf/rpm/package_query.cpp +++ b/libdnf/rpm/package_query.cpp @@ -227,6 +227,171 @@ static inline bool name_arch_compare_lower(const Solvable * first, const T * sec return first->arch < second->arch; } +void add_edges( + Base & base, + std::set & edges, + const PackageQuery & full_query, + const std::map & pkg_id2idx, + const ReldepList & deps) { + // resolve dependencies and add an edge if there is exactly one package satisfying it + for (int i = 0; i < deps.size(); ++i) { + ReldepList req_in_list(base); + req_in_list.add(deps.get_id(i)); + PackageQuery query(full_query); + query.filter_provides(req_in_list); + if (query.size() == 1) { + auto pkg = *query.begin(); + edges.insert(pkg_id2idx.at(pkg.get_id())); + } + } +} + +std::vector> build_graph( + Base & base, const PackageQuery & full_query, const std::vector & pkgs, bool use_recommends) { + // create pkg_id2idx to map Packages to their index in pkgs + std::map pkg_id2idx; + for (size_t i = 0; i < pkgs.size(); ++i) { + pkg_id2idx.emplace(pkgs[i].get_id(), i); + } + + std::vector> graph; + graph.reserve(pkgs.size()); + + for (unsigned int i = 0; i < pkgs.size(); ++i) { + std::set edges; + const auto & package = pkgs[i]; + add_edges(base, edges, full_query, pkg_id2idx, package.get_requires()); + if (use_recommends) { + add_edges(base, edges, full_query, pkg_id2idx, package.get_recommends()); + } + edges.erase(i); // remove self-edges + graph.emplace_back(edges.begin(), edges.end()); + } + + return graph; +} + +std::vector> reverse_graph(const std::vector> & graph) { + std::vector> rgraph(graph.size()); + + // pre-allocate (reserve) memory to prevent reallocations + std::vector lengths(graph.size(), 0); + for (const auto & edges : graph) { + for (auto edge : edges) { + ++lengths[edge]; + } + } + for (unsigned int i = 0; i < graph.size(); ++i) { + rgraph[i].reserve(lengths[i]); + } + + // reverse graph + for (unsigned int i = 0; i < graph.size(); ++i) { + for (auto edge : graph[i]) { + rgraph[edge].push_back(i); + } + } + + return rgraph; +} + +std::vector> kosaraju(const std::vector> & graph) { + const auto N = static_cast(graph.size()); + std::vector rstack(N); + std::vector stack(N); + std::vector tag(N, false); + unsigned int r = N; + unsigned int top = 0; + + // do depth-first searches in the graph and push nodes to rstack + // "on the way up" until all nodes have been pushed. + // tag nodes as they're processed so we don't visit them more than once + for (unsigned int i = 0; i < N; ++i) { + if (tag[i]) { + continue; + } + + unsigned int u = i; + unsigned int j = 0; + tag[u] = true; + while (true) { + const auto & edges = graph[u]; + if (j < edges.size()) { + const auto v = edges[j++]; + if (!tag[v]) { + rstack[top] = j; + stack[top++] = u; + u = v; + j = 0; + tag[u] = true; + } + } else { + rstack[--r] = u; + if (top == 0) { + break; + } + u = stack[--top]; + j = rstack[top]; + } + } + } + + if (r != 0) { + throw std::logic_error("leaves: kosaraju(): r != 0"); + } + + // now searches beginning at nodes popped from rstack in the graph with all + // edges reversed will give us the strongly connected components. + // this time all nodes are tagged, so let's remove the tags as we visit each + // node. + // the incoming edges to each component is the union of incoming edges to + // each node in the component minus the incoming edges from component nodes + // themselves. + // if there are no such incoming edges the component is a leaf and we + // add it to the array of leaves. + auto rgraph = reverse_graph(graph); + std::set sccredges; + std::vector> leaves; + for (; r < N; ++r) { + unsigned int u = rstack[r]; + if (!tag[u]) { + continue; + } + + stack[top++] = u; + tag[u] = false; + unsigned int s = N; + while (top) { + u = stack[--s] = stack[--top]; + const auto & redges = rgraph[u]; + for (unsigned int j = 0; j < redges.size(); ++j) { + const unsigned int v = redges[j]; + sccredges.insert(v); + if (!tag[v]) { + continue; + } + + stack[top++] = v; + tag[v] = false; + } + } + + for (unsigned int i = s; i < N; ++i) { + sccredges.erase(stack[i]); + } + + if (sccredges.empty()) { + std::vector scc(stack.begin() + s, stack.end()); + std::sort(scc.begin(), scc.end()); + leaves.emplace_back(std::move(scc)); + } else { + sccredges.clear(); + } + } + + return leaves; +} + } // namespace @@ -2487,6 +2652,54 @@ void PackageQuery::filter_duplicates() { } } +std::vector> PackageQuery::filter_leaves(bool return_grouped_leaves) { + std::vector> grouped_leaves; + auto & pool = get_rpm_pool(p_impl->base); + + // get array of all packages + std::vector pkgs(begin(), end()); + + // build the directed graph of dependencies + bool use_recommends = p_impl->base->get_config().get_install_weak_deps_option().get_value(); + auto graph = build_graph(*p_impl->base, *this, pkgs, use_recommends); + + // run Kosaraju's algorithm to find strongly connected components + // without any incoming edges + auto leaves = kosaraju(graph); + + libdnf::solv::SolvMap filter_result(pool.get_nsolvables()); + if (return_grouped_leaves) { + grouped_leaves.reserve(leaves.size()); + for (const auto & scc : leaves) { + auto & group = grouped_leaves.emplace_back(); + group.reserve(scc.size()); + for (unsigned int j = 0; j < scc.size(); ++j) { + const auto & package = pkgs[scc[j]]; + group.emplace_back(package); + filter_result.add_unsafe(package.get_id().id); + } + } + } else { + for (const auto & scc : leaves) { + for (unsigned int j = 0; j < scc.size(); ++j) { + const auto & package = pkgs[scc[j]]; + filter_result.add_unsafe(package.get_id().id); + } + } + } + *p_impl &= filter_result; + + return grouped_leaves; +} + +void PackageQuery::filter_leaves() { + filter_leaves(false); +} + +std::vector> PackageQuery::filter_leaves_groups() { + return filter_leaves(true); +} + void PackageQuery::filter_recent(const time_t timestamp) { auto & pool = get_rpm_pool(p_impl->base); const unsigned long long time_long = static_cast(timestamp);