diff --git a/bindings/libdnf5/rpm.i b/bindings/libdnf5/rpm.i index fb8d36872..f097b6448 100644 --- a/bindings/libdnf5/rpm.i +++ b/bindings/libdnf5/rpm.i @@ -44,6 +44,7 @@ #include "libdnf5/rpm/reldep_list_iterator.hpp" #include "libdnf5/rpm/rpm_signature.hpp" #include "libdnf5/rpm/transaction_callbacks.hpp" + #include "libdnf5/rpm/versionlock_config.hpp" %} #define CV __perl_CV @@ -59,6 +60,11 @@ %template(VectorNevraForm) std::vector; %template(PairBoolNevra) std::pair; +%include "libdnf5/rpm/versionlock_config.hpp" + +%template(VectorVersionlockCondition) std::vector; +%template(VectorVersionlockPackage) std::vector; + %include "libdnf5/rpm/package_sack.hpp" %template(PackageSackWeakPtr) libdnf5::WeakPtr; diff --git a/dnf5-plugins/changelog_plugin/changelog.cpp b/dnf5-plugins/changelog_plugin/changelog.cpp index 9d189c029..5cced3af3 100644 --- a/dnf5-plugins/changelog_plugin/changelog.cpp +++ b/dnf5-plugins/changelog_plugin/changelog.cpp @@ -119,7 +119,7 @@ void ChangelogCommand::run() { std::pair> filter = {libdnf5::cli::output::ChangelogFilterType::NONE, 0}; - libdnf5::rpm::PackageQuery full_package_query(ctx.base, libdnf5::sack::ExcludeFlags::APPLY_EXCLUDES, false); + libdnf5::rpm::PackageQuery full_package_query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); auto since = since_option->get_value(); auto count = count_option->get_value(); @@ -140,7 +140,7 @@ void ChangelogCommand::run() { } //query - libdnf5::rpm::PackageQuery query(ctx.base, libdnf5::sack::ExcludeFlags::APPLY_EXCLUDES, true); + libdnf5::rpm::PackageQuery query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK, true); libdnf5::ResolveSpecSettings settings{ .ignore_case = true, .with_nevra = true, diff --git a/dnf5/commands/advisory/advisory_info.cpp b/dnf5/commands/advisory/advisory_info.cpp index f0887ba75..af68015ec 100644 --- a/dnf5/commands/advisory/advisory_info.cpp +++ b/dnf5/commands/advisory/advisory_info.cpp @@ -30,7 +30,7 @@ using namespace libdnf5::cli; void AdvisoryInfoCommand::process_and_print_queries( Context & ctx, libdnf5::advisory::AdvisoryQuery & advisories, const std::vector & package_specs) { - libdnf5::rpm::PackageQuery packages(ctx.base); + libdnf5::rpm::PackageQuery packages(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); if (package_specs.size() > 0) { packages.filter_name(package_specs, libdnf5::sack::QueryCmp::IGLOB); } diff --git a/dnf5/commands/advisory/advisory_list.cpp b/dnf5/commands/advisory/advisory_list.cpp index 4bf458a8f..b41cb4703 100644 --- a/dnf5/commands/advisory/advisory_list.cpp +++ b/dnf5/commands/advisory/advisory_list.cpp @@ -32,7 +32,7 @@ void AdvisoryListCommand::process_and_print_queries( std::vector installed_pkgs; std::vector not_installed_pkgs; - libdnf5::rpm::PackageQuery packages(ctx.base); + libdnf5::rpm::PackageQuery packages(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); if (package_specs.size() > 0) { packages.filter_name(package_specs, libdnf5::sack::QueryCmp::IGLOB); } diff --git a/dnf5/commands/advisory/advisory_summary.cpp b/dnf5/commands/advisory/advisory_summary.cpp index abf4fe349..b896b4133 100644 --- a/dnf5/commands/advisory/advisory_summary.cpp +++ b/dnf5/commands/advisory/advisory_summary.cpp @@ -30,7 +30,7 @@ void AdvisorySummaryCommand::process_and_print_queries( Context & ctx, libdnf5::advisory::AdvisoryQuery & advisories, const std::vector & package_specs) { std::string mode; - libdnf5::rpm::PackageQuery packages(ctx.base); + libdnf5::rpm::PackageQuery packages(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); if (package_specs.size() > 0) { packages.filter_name(package_specs, libdnf5::sack::QueryCmp::IGLOB); } diff --git a/dnf5/commands/download/download.cpp b/dnf5/commands/download/download.cpp index 1f687b27c..03491d5ad 100644 --- a/dnf5/commands/download/download.cpp +++ b/dnf5/commands/download/download.cpp @@ -175,7 +175,7 @@ void DownloadCommand::run() { auto create_nevra_pkg_pair = [](const libdnf5::rpm::Package & pkg) { return std::make_pair(pkg.get_nevra(), pkg); }; std::map download_pkgs; - libdnf5::rpm::PackageQuery full_pkg_query(ctx.base); + libdnf5::rpm::PackageQuery full_pkg_query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); for (auto & pattern : *patterns_to_download_options) { libdnf5::rpm::PackageQuery pkg_query(full_pkg_query); auto option = dynamic_cast(pattern.get()); diff --git a/dnf5/commands/leaves/leaves.cpp b/dnf5/commands/leaves/leaves.cpp index 084ddec4b..b335082de 100644 --- a/dnf5/commands/leaves/leaves.cpp +++ b/dnf5/commands/leaves/leaves.cpp @@ -61,7 +61,7 @@ void LeavesCommand::configure() { void LeavesCommand::run() { auto & ctx = get_context(); - libdnf5::rpm::PackageQuery leaves_package_query(ctx.base); + libdnf5::rpm::PackageQuery leaves_package_query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); auto leaves_package_groups = leaves_package_query.filter_leaves_groups(); for (auto & package_group : leaves_package_groups) { diff --git a/dnf5/commands/list/list.cpp b/dnf5/commands/list/list.cpp index 524afd7b9..bc9c9b41f 100644 --- a/dnf5/commands/list/list.cpp +++ b/dnf5/commands/list/list.cpp @@ -145,8 +145,8 @@ void ListCommand::run() { auto & ctx = get_context(); auto & config = ctx.base.get_config(); - libdnf5::rpm::PackageQuery full_package_query(ctx.base); - libdnf5::rpm::PackageQuery base_query(ctx.base); + libdnf5::rpm::PackageQuery full_package_query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); + libdnf5::rpm::PackageQuery base_query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); // pre-select by patterns if (!pkg_specs.empty()) { diff --git a/dnf5/commands/provides/provides.cpp b/dnf5/commands/provides/provides.cpp index a3af4cf22..389e1296e 100644 --- a/dnf5/commands/provides/provides.cpp +++ b/dnf5/commands/provides/provides.cpp @@ -111,7 +111,7 @@ void ProvidesCommand::run() { std::set unmatched_specs; for (auto & spec : pkg_specs) { - libdnf5::rpm::PackageQuery full_package_query(ctx.base); + libdnf5::rpm::PackageQuery full_package_query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); // get the matched query first and the type of match (no_match, provides, file, binary) second auto matched = filter_spec(spec, full_package_query); for (auto package : matched.first) { diff --git a/dnf5/commands/repoquery/repoquery.cpp b/dnf5/commands/repoquery/repoquery.cpp index fa3f11176..e51159c18 100644 --- a/dnf5/commands/repoquery/repoquery.cpp +++ b/dnf5/commands/repoquery/repoquery.cpp @@ -45,7 +45,7 @@ libdnf5::rpm::PackageQuery repeat_filter( // Create source query of all considered packages. // To match dnf4 take arch filter into account. // (filtering by repo and available/installed is done implicitly by loading only the required metadata) - libdnf5::rpm::PackageQuery all_considered(candidates.get_base()); + libdnf5::rpm::PackageQuery all_considered(candidates.get_base(), libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); if (!arches.empty()) { all_considered.filter_arch(arches, libdnf5::sack::QueryCmp::GLOB); } @@ -553,6 +553,9 @@ void RepoqueryCommand::run() { libdnf5::sack::ExcludeFlags flags = disable_modular_filtering->get_value() ? libdnf5::sack::ExcludeFlags::IGNORE_MODULAR_EXCLUDES : libdnf5::sack::ExcludeFlags::APPLY_EXCLUDES; + if (!upgrades->get_value()) { + flags = flags | libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK; + } libdnf5::rpm::PackageQuery base_query(ctx.base, flags, false); libdnf5::rpm::PackageQuery result_query(ctx.base, flags, true); diff --git a/dnf5/commands/search/search_processor.cpp b/dnf5/commands/search/search_processor.cpp index 422db41df..5d2407d1f 100644 --- a/dnf5/commands/search/search_processor.cpp +++ b/dnf5/commands/search/search_processor.cpp @@ -80,7 +80,7 @@ SearchProcessor::SearchProcessor( : base(base), patterns(patterns), search_all(search_all), - full_package_query(base), + full_package_query(base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK), showdupes(show_duplicates) { if (!showdupes) { full_package_query.filter_latest_evr(); diff --git a/dnf5/commands/versionlock/utils.cpp b/dnf5/commands/versionlock/utils.cpp new file mode 100644 index 000000000..6e6b6eef4 --- /dev/null +++ b/dnf5/commands/versionlock/utils.cpp @@ -0,0 +1,35 @@ +/* +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 "utils.hpp" + +#include +#include + +#include + +std::string format_comment(std::string_view cmd) { + // format the comment for new config file entries + auto current_time_point = std::chrono::system_clock::now(); + const std::time_t current_time = std::chrono::system_clock::to_time_t(current_time_point); + std::stringstream ss; + ss << std::put_time(std::localtime(¤t_time), "%F %T"); + // TODO(mblaha): add full command line + return libdnf5::utils::sformat(_("Added by 'versionlock {}' command on {}"), cmd, ss.str()); +} diff --git a/dnf5/commands/versionlock/utils.hpp b/dnf5/commands/versionlock/utils.hpp new file mode 100644 index 000000000..df9178e8d --- /dev/null +++ b/dnf5/commands/versionlock/utils.hpp @@ -0,0 +1,27 @@ +/* +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_VERSIONLOCK_UTILS_HPP +#define DNF5_COMMANDS_VERSIONLOCK_UTILS_HPP + +#include + +std::string format_comment(std::string_view cmd); + +#endif // DNF5_COMMANDS_VERSIONLOCK_UTILS_HPP diff --git a/dnf5/commands/versionlock/versionlock.cpp b/dnf5/commands/versionlock/versionlock.cpp new file mode 100644 index 000000000..1d0bb0fcc --- /dev/null +++ b/dnf5/commands/versionlock/versionlock.cpp @@ -0,0 +1,57 @@ +/* +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 "versionlock.hpp" + +#include "versionlock_add.hpp" +#include "versionlock_clear.hpp" +#include "versionlock_delete.hpp" +#include "versionlock_exclude.hpp" +#include "versionlock_list.hpp" + +namespace dnf5 { + +void VersionlockCommand::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("subcommands").register_argument(arg_parser_this_cmd); +} + +void VersionlockCommand::set_argument_parser() { + get_argument_parser_command()->set_description("Manage versionlock configuration"); +} + +void VersionlockCommand::register_subcommands() { + auto * commands_group = get_context().get_argument_parser().add_new_group("versionlock_commands"); + commands_group->set_header("Versionlock Commands:"); + get_argument_parser_command()->register_group(commands_group); + register_subcommand(std::make_unique(get_context()), commands_group); + register_subcommand(std::make_unique(get_context()), commands_group); + register_subcommand(std::make_unique(get_context()), commands_group); + register_subcommand(std::make_unique(get_context()), commands_group); + register_subcommand(std::make_unique(get_context()), commands_group); +} + +void VersionlockCommand::pre_configure() { + throw_missing_command(); +} + + +} // namespace dnf5 diff --git a/dnf5/commands/versionlock/versionlock.hpp b/dnf5/commands/versionlock/versionlock.hpp new file mode 100644 index 000000000..7424468fa --- /dev/null +++ b/dnf5/commands/versionlock/versionlock.hpp @@ -0,0 +1,38 @@ +/* +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_VERSIONLOCK_VERSIONLOCK_HPP +#define DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_HPP + +#include + +namespace dnf5 { + +class VersionlockCommand : public Command { +public: + explicit VersionlockCommand(Context & context) : Command(context, "versionlock") {} + void set_parent_command() override; + void set_argument_parser() override; + void register_subcommands() override; + void pre_configure() override; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_HPP diff --git a/dnf5/commands/versionlock/versionlock_add.cpp b/dnf5/commands/versionlock/versionlock_add.cpp new file mode 100644 index 000000000..eb74e059c --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_add.cpp @@ -0,0 +1,133 @@ +/* +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 "versionlock_add.hpp" + +#include "utils.hpp" + +#include +#include +#include +#include + +#include + +namespace dnf5 { + +using namespace libdnf5::cli; + +void VersionlockAddCommand::set_argument_parser() { + auto & cmd = *get_argument_parser_command(); + cmd.set_description(_("Add new entry to versionlock configuration")); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto * keys = parser.add_new_positional_arg("specs", ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr); + keys->set_description(_("List of package specs to add versionlock for")); + keys->set_parse_hook_func( + [this]([[maybe_unused]] ArgumentParser::PositionalArg * arg, int argc, const char * const argv[]) { + for (int i = 0; i < argc; ++i) { + pkg_specs.emplace_back(argv[i]); + } + return true; + }); + cmd.register_positional_arg(keys); +} + +void VersionlockAddCommand::configure() { + auto & context = get_context(); + context.set_load_system_repo(true); + context.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); +} + +bool lock_version( + libdnf5::rpm::VersionlockConfig & vl_config, const libdnf5::rpm::Package & pkg, const std::string & comment) { + auto evr = pkg.get_evr(); + auto name = pkg.get_name(); + auto & vl_packages = vl_config.get_packages(); + // check whether a rule for this version is already present + for (const auto & vl_package : vl_packages) { + if (!vl_package.is_valid() || vl_package.get_name() != name) { + continue; + } + for (const auto & vl_cond : vl_package.get_conditions()) { + if (!vl_cond.is_valid()) { + continue; + } + if (vl_cond.get_key() == libdnf5::rpm::VersionlockCondition::Keys::EVR && + vl_cond.get_comparator() == libdnf5::sack::QueryCmp::EQ && vl_cond.get_value() == evr) { + // do not add duplicite versionlock rules + return false; + } + } + } + + const libdnf5::rpm::VersionlockCondition vl_condition("evr", "=", evr); + libdnf5::rpm::VersionlockPackage vl_package(name, std::vector{vl_condition}); + vl_package.set_comment(comment); + vl_packages.emplace_back(std::move(vl_package)); + return true; +} + +void VersionlockAddCommand::run() { + auto & ctx = get_context(); + auto package_sack = ctx.base.get_rpm_package_sack(); + auto vl_config = package_sack->get_versionlock_config(); + auto orig_size = vl_config.get_packages().size(); + + const auto comment = format_comment("add"); + + const libdnf5::ResolveSpecSettings settings{ + .with_nevra = true, .with_provides = false, .with_filenames = false, .with_binaries = false}; + for (const auto & spec : pkg_specs) { + libdnf5::rpm::PackageQuery query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); + query.resolve_pkg_spec(spec, settings, false); + if (query.empty()) { + std::cerr << libdnf5::utils::sformat(_("No package found for \"{}\"."), spec) << std::endl; + continue; + } + libdnf5::rpm::PackageQuery installed_query(query); + installed_query.filter_installed(); + if (!installed_query.empty()) { + // if spec is installed, add only installed version + query = installed_query; + } + + std::unordered_set versions{}; + for (const auto & pkg : query) { + auto evr = pkg.get_evr(); + if (versions.contains(evr)) { + continue; + } + versions.emplace(evr); + if (lock_version(vl_config, pkg, comment)) { + std::cout << libdnf5::utils::sformat(_("Adding versionlock on \"{0} = {1}\"."), pkg.get_name(), evr) + << std::endl; + } else { + std::cerr << libdnf5::utils::sformat(_("Package \"{}\" is already locked."), spec) << std::endl; + } + } + } + if (vl_config.get_packages().size() != orig_size) { + vl_config.save(); + } +} + +} // namespace dnf5 diff --git a/dnf5/commands/versionlock/versionlock_add.hpp b/dnf5/commands/versionlock/versionlock_add.hpp new file mode 100644 index 000000000..5955b6824 --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_add.hpp @@ -0,0 +1,47 @@ +/* +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_VERSIONLOCK_VERSIONLOCK_ADD_HPP +#define DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_ADD_HPP + + +#include + + +namespace dnf5 { + + +class VersionlockAddCommand : public Command { +public: + explicit VersionlockAddCommand(Context & context) : VersionlockAddCommand(context, "add") {} + void set_argument_parser() override; + void configure() override; + void run() override; + +protected: + VersionlockAddCommand(Context & context, const std::string & name) : Command(context, name) {} + +private: + std::vector pkg_specs; +}; + +} // namespace dnf5 + + +#endif // DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_ADD_HPP diff --git a/dnf5/commands/versionlock/versionlock_clear.cpp b/dnf5/commands/versionlock/versionlock_clear.cpp new file mode 100644 index 000000000..7553ce1a5 --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_clear.cpp @@ -0,0 +1,43 @@ +/* +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 "versionlock_clear.hpp" + +#include + +#include + +namespace dnf5 { + +using namespace libdnf5::cli; + +void VersionlockClearCommand::set_argument_parser() { + auto & cmd = *get_argument_parser_command(); + cmd.set_description(_("Remove all entries from versionlock configuration")); +} + +void VersionlockClearCommand::run() { + auto & ctx = get_context(); + auto package_sack = ctx.base.get_rpm_package_sack(); + auto vl_config = package_sack->get_versionlock_config(); + vl_config.get_packages().clear(); + vl_config.save(); +} + +} // namespace dnf5 diff --git a/dnf5/commands/versionlock/versionlock_clear.hpp b/dnf5/commands/versionlock/versionlock_clear.hpp new file mode 100644 index 000000000..be12196d6 --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_clear.hpp @@ -0,0 +1,43 @@ +/* +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_VERSIONLOCK_VERSIONLOCK_CLEAR_HPP +#define DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_CLEAR_HPP + + +#include + + +namespace dnf5 { + + +class VersionlockClearCommand : public Command { +public: + explicit VersionlockClearCommand(Context & context) : VersionlockClearCommand(context, "clear") {} + void set_argument_parser() override; + void run() override; + +protected: + VersionlockClearCommand(Context & context, const std::string & name) : Command(context, name) {} +}; + +} // namespace dnf5 + + +#endif // DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_CLEAR_HPP diff --git a/dnf5/commands/versionlock/versionlock_delete.cpp b/dnf5/commands/versionlock/versionlock_delete.cpp new file mode 100644 index 000000000..d81fa2d86 --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_delete.cpp @@ -0,0 +1,80 @@ +/* +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 "versionlock_delete.hpp" + +#include +#include +#include +#include + +#include + +namespace dnf5 { + +using namespace libdnf5::cli; + +void VersionlockDeleteCommand::set_argument_parser() { + auto & cmd = *get_argument_parser_command(); + cmd.set_description(_("Remove any matching versionlock configuration entries")); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto * keys = parser.add_new_positional_arg("specs", ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr); + keys->set_description(_("List of package specs to remove versionlock for")); + keys->set_parse_hook_func( + [this]([[maybe_unused]] ArgumentParser::PositionalArg * arg, int argc, const char * const argv[]) { + for (int i = 0; i < argc; ++i) { + pkg_specs.emplace_back(argv[i]); + } + return true; + }); + cmd.register_positional_arg(keys); +} + +void delete_package(libdnf5::rpm::VersionlockConfig & vl_config, std::string_view spec) { + auto remove_predicate = [spec](libdnf5::rpm::VersionlockPackage & pkg) { + if (pkg.is_valid() && pkg.get_name() == spec) { + std::cout << _("Deleting versionlock entry:") << std::endl; + std::cout << pkg.to_string(false, true) << std::endl; + return true; + } + return false; + }; + + auto & vl_packages = vl_config.get_packages(); + vl_packages.erase(std::remove_if(vl_packages.begin(), vl_packages.end(), remove_predicate), vl_packages.end()); +} + +void VersionlockDeleteCommand::run() { + auto & ctx = get_context(); + auto package_sack = ctx.base.get_rpm_package_sack(); + auto vl_config = package_sack->get_versionlock_config(); + auto orig_size = vl_config.get_packages().size(); + + for (const auto & spec : pkg_specs) { + delete_package(vl_config, spec); + } + if (vl_config.get_packages().size() != orig_size) { + vl_config.save(); + } +} + +} // namespace dnf5 diff --git a/dnf5/commands/versionlock/versionlock_delete.hpp b/dnf5/commands/versionlock/versionlock_delete.hpp new file mode 100644 index 000000000..9948819be --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_delete.hpp @@ -0,0 +1,46 @@ +/* +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_VERSIONLOCK_VERSIONLOCK_DELETE_HPP +#define DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_DELETE_HPP + + +#include + + +namespace dnf5 { + + +class VersionlockDeleteCommand : public Command { +public: + explicit VersionlockDeleteCommand(Context & context) : VersionlockDeleteCommand(context, "delete") {} + void set_argument_parser() override; + void run() override; + +protected: + VersionlockDeleteCommand(Context & context, const std::string & name) : Command(context, name) {} + +private: + std::vector pkg_specs; +}; + +} // namespace dnf5 + + +#endif // DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_DELETE_HPP diff --git a/dnf5/commands/versionlock/versionlock_exclude.cpp b/dnf5/commands/versionlock/versionlock_exclude.cpp new file mode 100644 index 000000000..80a7b7c09 --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_exclude.cpp @@ -0,0 +1,145 @@ +/* +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 "versionlock_exclude.hpp" + +#include "utils.hpp" + +#include +#include +#include +#include + +#include + +namespace dnf5 { + +using namespace libdnf5::cli; + +void VersionlockExcludeCommand::set_argument_parser() { + auto & cmd = *get_argument_parser_command(); + cmd.set_description(_("Add new exclude entry to versionlock configuration")); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto * keys = parser.add_new_positional_arg("specs", ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr); + keys->set_description(_("List of package specs to add versionlock exclude for")); + keys->set_parse_hook_func( + [this]([[maybe_unused]] ArgumentParser::PositionalArg * arg, int argc, const char * const argv[]) { + for (int i = 0; i < argc; ++i) { + pkg_specs.emplace_back(argv[i]); + } + return true; + }); + cmd.register_positional_arg(keys); +} + +void VersionlockExcludeCommand::configure() { + auto & context = get_context(); + context.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); +} + +bool exclude_versions( + libdnf5::rpm::VersionlockConfig & vl_config, + std::string_view name, + const std::vector & packages, + const std::string & comment) { + auto & vl_packages = vl_config.get_packages(); + // find entry with same name and only != operators + for (auto & vl_package : vl_packages) { + if (!vl_package.is_valid() || vl_package.get_name() != name) { + continue; + } + auto vl_conditions = vl_package.get_conditions(); + if (std::any_of(vl_conditions.begin(), vl_conditions.end(), [](const auto & vl_cond) { + return vl_cond.get_key() != libdnf5::rpm::VersionlockCondition::Keys::EVR || + vl_cond.get_comparator() != libdnf5::sack::QueryCmp::NEQ; + })) { + continue; + } + // add missing versions and return + bool changed = false; + for (const auto & pkg : packages) { + const auto evr = pkg.get_evr(); + if (std::any_of(vl_conditions.begin(), vl_conditions.end(), [&evr](const auto & vl_cond) { + return vl_cond.get_value() == evr; + })) { + // condition for this evr is already present + continue; + } + vl_package.add_condition(libdnf5::rpm::VersionlockCondition{"evr", "!=", evr}); + std::cout << libdnf5::utils::sformat(_("Adding versionlock exclude on \"{0} = {1}\"."), name, evr) + << std::endl; + changed = true; + } + return changed; + } + + // add new entry with all versions and return + std::vector conditions; + for (const auto & pkg : packages) { + const auto evr = pkg.get_evr(); + conditions.emplace_back("evr", "!=", evr); + std::cout << libdnf5::utils::sformat(_("Adding versionlock exclude on \"{0} = {1}\"."), name, evr) << std::endl; + } + libdnf5::rpm::VersionlockPackage vl_package(name, std::move(conditions)); + vl_package.set_comment(comment); + vl_packages.emplace_back(std::move(vl_package)); + return true; +} + +void VersionlockExcludeCommand::run() { + auto & ctx = get_context(); + auto package_sack = ctx.base.get_rpm_package_sack(); + auto vl_config = package_sack->get_versionlock_config(); + + const auto comment = format_comment("exclude"); + + bool changed{false}; + const libdnf5::ResolveSpecSettings settings{ + .with_nevra = true, .with_provides = false, .with_filenames = false, .with_binaries = false}; + for (const auto & spec : pkg_specs) { + libdnf5::rpm::PackageQuery query(ctx.base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); + query.resolve_pkg_spec(spec, settings, false); + if (query.empty()) { + std::cerr << libdnf5::utils::sformat(_("No package found for \"{}\"."), spec) << std::endl; + continue; + } + + // group packages by name + std::unordered_map> versions{}; + for (const auto & pkg : query) { + versions[pkg.get_name()].emplace_back(pkg); + } + + for (const auto & [name, packages] : versions) { + if (exclude_versions(vl_config, name, packages, comment)) { + changed = true; + } else { + std::cerr << libdnf5::utils::sformat(_("Package \"{}\" is already excluded."), spec) << std::endl; + } + } + } + if (changed) { + vl_config.save(); + } +} + +} // namespace dnf5 diff --git a/dnf5/commands/versionlock/versionlock_exclude.hpp b/dnf5/commands/versionlock/versionlock_exclude.hpp new file mode 100644 index 000000000..dd3d0a76f --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_exclude.hpp @@ -0,0 +1,47 @@ +/* +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_VERSIONLOCK_VERSIONLOCK_EXCLUDE_HPP +#define DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_EXCLUDE_HPP + + +#include + + +namespace dnf5 { + + +class VersionlockExcludeCommand : public Command { +public: + explicit VersionlockExcludeCommand(Context & context) : VersionlockExcludeCommand(context, "exclude") {} + void set_argument_parser() override; + void configure() override; + void run() override; + +protected: + VersionlockExcludeCommand(Context & context, const std::string & name) : Command(context, name) {} + +private: + std::vector pkg_specs; +}; + +} // namespace dnf5 + + +#endif // DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_EXCLUDE_HPP diff --git a/dnf5/commands/versionlock/versionlock_list.cpp b/dnf5/commands/versionlock/versionlock_list.cpp new file mode 100644 index 000000000..74ae7dc69 --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_list.cpp @@ -0,0 +1,48 @@ +/* +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 "versionlock_list.hpp" + +#include + +namespace dnf5 { + +using namespace libdnf5::cli; + +void VersionlockListCommand::set_argument_parser() { + auto & cmd = *get_argument_parser_command(); + cmd.set_description("List the current versionlock configuration"); +} + +void VersionlockListCommand::run() { + auto & ctx = get_context(); + auto package_sack = ctx.base.get_rpm_package_sack(); + auto vl_config = package_sack->get_versionlock_config(); + + bool first = true; + for (const auto & vl_pkg : vl_config.get_packages()) { + if (!first) { + std::cout << std::endl; + } + std::cout << vl_pkg.to_string(true, true) << std::endl; + first = false; + } +} + +} // namespace dnf5 diff --git a/dnf5/commands/versionlock/versionlock_list.hpp b/dnf5/commands/versionlock/versionlock_list.hpp new file mode 100644 index 000000000..4fc0a3b39 --- /dev/null +++ b/dnf5/commands/versionlock/versionlock_list.hpp @@ -0,0 +1,44 @@ +/* +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_VERSIONLOCK_VERSIONLOCK_LIST_HPP +#define DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_LIST_HPP + + +#include + + +namespace dnf5 { + + +class VersionlockListCommand : public Command { +public: + explicit VersionlockListCommand(Context & context) : VersionlockListCommand(context, "list") {} + void set_argument_parser() override; + void run() override; + +protected: + VersionlockListCommand(Context & context, const std::string & name) : Command(context, name) {} +}; + + +} // namespace dnf5 + + +#endif // DNF5_COMMANDS_VERSIONLOCK_VERSIONLOCK_LIST_HPP diff --git a/dnf5/main.cpp b/dnf5/main.cpp index b58563c8f..ee78b8188 100644 --- a/dnf5/main.cpp +++ b/dnf5/main.cpp @@ -44,6 +44,7 @@ along with libdnf. If not, see . #include "commands/search/search.hpp" #include "commands/swap/swap.hpp" #include "commands/upgrade/upgrade.hpp" +#include "commands/versionlock/versionlock.hpp" #include "dnf5/context.hpp" #include "download_callbacks.hpp" #include "plugins.hpp" @@ -689,6 +690,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)); } static void load_plugins(Context & context) { diff --git a/dnf5daemon-server/services/advisory/advisory.cpp b/dnf5daemon-server/services/advisory/advisory.cpp index a7faa1c86..95f0fdf02 100644 --- a/dnf5daemon-server/services/advisory/advisory.cpp +++ b/dnf5daemon-server/services/advisory/advisory.cpp @@ -92,7 +92,7 @@ libdnf5::advisory::AdvisoryQuery Advisory::advisory_query_from_options( advisories.filter_reference("*", {"cve"}, libdnf5::sack::QueryCmp::IGLOB); } - libdnf5::rpm::PackageQuery package_query(base); + libdnf5::rpm::PackageQuery package_query(base, libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK); auto opt_contains_pkgs = key_value_map_get>(options, "contains_pkgs", {}); if (!opt_contains_pkgs.empty()) { package_query.filter_name(opt_contains_pkgs, libdnf5::sack::QueryCmp::IGLOB); diff --git a/dnf5daemon-server/services/rpm/rpm.cpp b/dnf5daemon-server/services/rpm/rpm.cpp index 5e30eb53c..e8cfdb56c 100644 --- a/dnf5daemon-server/services/rpm/rpm.cpp +++ b/dnf5daemon-server/services/rpm/rpm.cpp @@ -90,13 +90,17 @@ sdbus::MethodReply Rpm::list(sdbus::MethodCall & call) { session.fill_sack(); auto base = session.get_base(); + std::string scope = key_value_map_get(options, "scope", "all"); // start with all packages - libdnf5::rpm::PackageQuery query(*base); + libdnf5::sack::ExcludeFlags flags = libdnf5::sack::ExcludeFlags::APPLY_EXCLUDES; + if (scope != "upgrades" && scope != "upgradable") { + flags = flags | libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK; + } + libdnf5::rpm::PackageQuery query(*base, flags); // toplevel filtering - the scope // TODO(mblaha): support for other possible scopes? // userinstalled, duplicates, unneeded, extras, installonly, recent, unsatisfied - std::string scope = key_value_map_get(options, "scope", "all"); if (scope == "installed") { query.filter_installed(); } else if (scope == "available") { diff --git a/doc/commands/index.rst b/doc/commands/index.rst index 94bb76066..d38a112a7 100644 --- a/doc/commands/index.rst +++ b/doc/commands/index.rst @@ -27,6 +27,7 @@ DNF5 Commands search.8 swap.8 upgrade.8 + versionlock.8 .. # TODO(jkolarik): history not ready yet diff --git a/doc/commands/versionlock.8.rst b/doc/commands/versionlock.8.rst new file mode 100644 index 000000000..55e3bb31d --- /dev/null +++ b/doc/commands/versionlock.8.rst @@ -0,0 +1,136 @@ +.. + 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 . + +.. _versionlock_command_ref-label: + +#################### + Versionlock Command +#################### + +Synopsis +======== + +``dnf5 versionlock ...`` + + +Description +=========== + +The ``versionlock`` command in ``DNF5`` takes a set of names and versions for +packages and excludes all other versions of those packages. This allows you to +protect packages from being updated by newer versions. Alternately, it accepts +a specific package version to exclude from updates, e.g. for when it's +necessary to skip a specific release of a package that has known issues. + +The plugin will walk each entry of the versionlock file, and exclude any +package of given name that doesn't match conditions listed within the file. +This is basically the same as using `dnf5 --exclude` for the package name itself +(as you cannot exclude installed packages), but dnf will still see the versions +you have installed/versionlocked as available so that `dnf reinstall` will +still work. + +Note the versionlock command does not apply any excludes in non-transactional +operations like `repoquery`, `list`, `info`, etc. + + +Subcommands +=========== + +``add`` + | Add a versionlock for all available packages matching the spec. It means that only versions of packages represented by ``package-spec`` are available for transaction operations. The NEVRAs to lock to are first searched among installed packages and then (if none is found) in all currently available packages. + +``exclude`` + | Add an exclude (within versionlock) for the available packages matching the spec. It means that packages represented by ``package-spec`` will be excluded from transaction operations. + +``delete`` + | Remove any matching versionlock entries. + +``list`` + | List the current versionlock entries. + +``clear`` + | Remove all versionlock entries. + + +Examples +======== + +``dnf5 versionlock add acpi`` + | If acpi package is installed, lock it to currently installed version. If it's not installed, lock acpi to any of currently available versions. + +``dnf5 versionlock list`` + | Show current versionlock configuration. + +``dnf5 versionlock delete acpi`` + | Remove any rules for acpi package. + +``dnf5 versionlock exclude iftop-1.2.3-7.fc38`` + | Exclude iftop-1.2.3-7.fc38 release. + + +Versionlock file format +======================= + +The versionlock file is a TOML file stored in location `/etc/dnf/versionlock.toml`. +The file must contain the `version` key, currently supported version is `1.0`. +Then it contains `packages` - a list of locking entries. Each entry consist of the package name and a list of conditions. All the conditions must be true for a package to match (they are combined using logical AND). All entries are then combined together using logical OR operation. + + +Example of versionlock file +--------------------------- + + +.. code-block:: toml + + version = "1.0" + + # keep package bash on version 0:5.2.15-5.fc39 + [[packages]] + name = "bash" # name of the package + comment = "description" # optional description of the entry + [[packages.conditions]] # conditions for the package "bash" + key = "evr" # epoch, version, evr, and arch keys are supported + comparator = "=" # <, <=, =, >=, >, and != operators are supported + value = "0:5.2.15-5.fc39" # pattern to match + + # exclude iftop-1.2.3-7.fc38 version (versionlock exclude iftop-1.2.3-7.fc38) + [[packages]] + name = "iftop" + [[packages.conditions]] + key = "evr" + comparator = "!=" + value = "0:1.0-0.31.pre4.fc39" + + # keep acpi on major version 3 + [[packages]] + name = "acpi" + [[packages.conditions]] + key = "evr" + comparator = "<" + value = "4" + [[packages.conditions]] + key = "evr" + comparator = ">=" + value = "3" + + + +See Also +======== + + | :manpage:`dnf5-specs(7)`, :ref:`Patterns specification ` diff --git a/include/libdnf5/base/goal_elements.hpp b/include/libdnf5/base/goal_elements.hpp index 54cd42959..0815733cb 100644 --- a/include/libdnf5/base/goal_elements.hpp +++ b/include/libdnf5/base/goal_elements.hpp @@ -82,7 +82,8 @@ enum class GoalProblem : uint32_t { SOLVER_PROBLEM_STRICT_RESOLVEMENT = (1 << 13), WRITE_DEBUG = (1 << 14), UNSUPPORTED_ACTION = (1 << 15), - MULTIPLE_STREAMS = (1 << 16) + MULTIPLE_STREAMS = (1 << 16), + EXCLUDED_VERSIONLOCK = (1 << 17) }; /// Types of Goal actions diff --git a/include/libdnf5/common/sack/exclude_flags.hpp b/include/libdnf5/common/sack/exclude_flags.hpp index 1d6cdd3d8..cb5816c97 100644 --- a/include/libdnf5/common/sack/exclude_flags.hpp +++ b/include/libdnf5/common/sack/exclude_flags.hpp @@ -31,8 +31,9 @@ enum class ExcludeFlags : unsigned { IGNORE_REGULAR_CONFIG_EXCLUDES = 1 << 1, IGNORE_REGULAR_USER_EXCLUDES = 1 << 2, USE_DISABLED_REPOSITORIES = 1 << 3, + IGNORE_VERSIONLOCK = 1 << 4, IGNORE_REGULAR_EXCLUDES = IGNORE_REGULAR_CONFIG_EXCLUDES | IGNORE_REGULAR_USER_EXCLUDES, - IGNORE_EXCLUDES = IGNORE_MODULAR_EXCLUDES | IGNORE_REGULAR_EXCLUDES | USE_DISABLED_REPOSITORIES + IGNORE_EXCLUDES = IGNORE_MODULAR_EXCLUDES | IGNORE_REGULAR_EXCLUDES | USE_DISABLED_REPOSITORIES | IGNORE_VERSIONLOCK }; inline ExcludeFlags operator&(ExcludeFlags lhs, ExcludeFlags rhs) { @@ -41,6 +42,12 @@ inline ExcludeFlags operator&(ExcludeFlags lhs, ExcludeFlags rhs) { static_cast::type>(rhs)); } +inline ExcludeFlags operator|(ExcludeFlags lhs, ExcludeFlags rhs) { + return static_cast( + static_cast::type>(lhs) | + static_cast::type>(rhs)); +} + } // namespace libdnf5::sack #endif // LIBDNF5_COMMON_SACK_EXCLUDE_FLAGS_HPP diff --git a/include/libdnf5/conf/const.hpp b/include/libdnf5/conf/const.hpp index 6f64a6789..0e6f6cff7 100644 --- a/include/libdnf5/conf/const.hpp +++ b/include/libdnf5/conf/const.hpp @@ -34,6 +34,8 @@ constexpr const char * SYSTEM_CACHEDIR = "/var/cache/libdnf5"; constexpr const char * CONF_FILENAME = "/etc/dnf/dnf.conf"; constexpr const char * CONF_DIRECTORY = "/etc/dnf/libdnf5.conf.d"; +constexpr const char * VERSIONLOCK_CONF_FILENAME = "/etc/dnf/versionlock.toml"; + constexpr const char * PLUGINS_CONF_DIR = "/etc/dnf/libdnf5-plugins"; const std::vector REPOSITORY_CONF_DIRS{ diff --git a/include/libdnf5/rpm/package_query.hpp b/include/libdnf5/rpm/package_query.hpp index a60d21d65..d49e87465 100644 --- a/include/libdnf5/rpm/package_query.hpp +++ b/include/libdnf5/rpm/package_query.hpp @@ -159,7 +159,7 @@ class PackageQuery : public PackageSet { /// /// @param patterns A vector of strings the filter is matched against. /// @param cmp_type A comparison (match) operator, defaults to `QueryCmp::EQ`. - /// Supported values: `EQ`, `GT`, `LT`, `GTE`, `LTE`, `EQ`. + /// Supported values: `EQ`, `GT`, `LT`, `GTE`, `LTE`, `NEQ`. /// @since 5.0 // // @replaces libdnf/sack/query.hpp:method:addFilter(int keyname, int cmp_type, const char *match) - cmp_type = HY_PKG_EVR @@ -715,6 +715,12 @@ class PackageQuery : public PackageSet { /// Filter packages that provide a capability that matches with any value in installonlypkgs configuration option. void filter_installonly(); + /// Filter out versionlock excluded packages. + /// + /// The packages versions excluded by versionlock are removed from the query. + /// @since 5.1.13 + void filter_versionlock(); + private: std::vector> filter_leaves(bool return_grouped_leaves); diff --git a/include/libdnf5/rpm/package_sack.hpp b/include/libdnf5/rpm/package_sack.hpp index 3599a393d..a371dc322 100644 --- a/include/libdnf5/rpm/package_sack.hpp +++ b/include/libdnf5/rpm/package_sack.hpp @@ -28,6 +28,7 @@ along with libdnf. If not, see . #include "libdnf5/base/base_weak.hpp" #include "libdnf5/common/exception.hpp" #include "libdnf5/common/weak_ptr.hpp" +#include "libdnf5/rpm/versionlock_config.hpp" #include "libdnf5/transaction/transaction_item_reason.hpp" #include @@ -145,6 +146,34 @@ class PackageSack { /// @since 5.0 void clear_user_includes(); + + /// Returns versionlock configuration + /// @since 5.1.13 + VersionlockConfig get_versionlock_config() const; + + /// Returns versionlock excluded package set + /// @since 5.1.13 + const PackageSet get_versionlock_excludes(); + + /// Add package set to versionlock excluded packages + /// @param excludes: packages to add to excludes + /// @since 5.1.13 + void add_versionlock_excludes(const PackageSet & excludes); + + /// Remove package set from versionlock excluded packages + /// @param excludes: packages to remove from excludes + /// @since 5.1.13 + void remove_versionlock_excludes(const PackageSet & excludes); + + /// Resets versionlock excluded packages to a new value + /// @param excludes: packages to exclude + /// @since 5.1.13 + void set_versionlock_excludes(const PackageSet & excludes); + + /// Clear versionlock excluded packages + /// @since 5.1.13 + void clear_versionlock_excludes(); + rpm::Package get_running_kernel(); private: diff --git a/include/libdnf5/rpm/versionlock_config.hpp b/include/libdnf5/rpm/versionlock_config.hpp new file mode 100644 index 000000000..f20807948 --- /dev/null +++ b/include/libdnf5/rpm/versionlock_config.hpp @@ -0,0 +1,147 @@ +/* +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 Lesser General Public License as published by +the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with libdnf. If not, see . +*/ + + +#ifndef LIBDNF5_RPM_VERSIONLOCK_CONFIG_HPP +#define LIBDNF5_RPM_VERSIONLOCK_CONFIG_HPP + +#include "libdnf5/common/sack/query_cmp.hpp" + +#include +#include +#include +#include + +namespace libdnf5::rpm { + +/// A condition for the versionlock package. +/// Each condition consist of three parts: key, comparison operator, and value. +/// Key can be one of "epoch", "evr", "arch". +/// Supported comparison operators are "<", "<=", "=", ">=", ">", "!=". +/// @since 5.1.13 +class VersionlockCondition { +public: + enum class Keys { EPOCH, EVR, ARCH }; + + VersionlockCondition(const std::string & key_str, const std::string & comparator_str, const std::string & value); + + /// Returns true if this configuration entry is valid - contains supported values + /// in all three parts (key, operator, and value). + bool is_valid() const { return valid; } + + /// Get the key (which part of a package is compared). + Keys get_key() const { return key; } + /// Get the comparison operator. + libdnf5::sack::QueryCmp get_comparator() const { return comparator; } + /// Get the value. + std::string get_value() const { return value; } + + /// Get the key as a string. + std::string get_key_str() const { return key_str; } + /// Get the comparison operator as a string. + std::string get_comparator_str() const { return comparator_str; } + + /// Get list of errors found during parsing the entry from configuration file. + const std::vector & get_errors() const { return errors; } + + /// Converts the condition to "key operator value" string usable for printing. + /// @param with_errors Include also error messages for invalid entries + std::string to_string(bool with_errors) const; + +private: + // map string comparator to query cmp_type + static const std::map VALID_COMPARATORS; + bool valid; + std::string key_str; + Keys key; + std::string comparator_str; + libdnf5::sack::QueryCmp comparator{0}; + std::string value; + std::vector errors{}; +}; + +/// One versionlock configuration file entry. It consists of the +/// package name and a set of conditions. All conditions must be true +/// for package version to get locked. +/// @since 5.1.13 +class VersionlockPackage { +public: + /// Creates an instance of `VersionlockPackage` class specifying the + /// name of package. + /// @param name Name of the package to be configured + VersionlockPackage(std::string_view name, std::vector && conditions); + + /// Returns true if this configuration entry is valid. + bool is_valid() const { return valid; } + + /// Get the package name. + std::string get_name() const { return name; } + + /// Get the comment for this entry. + std::string get_comment() const { return comment; } + /// Set comment for this entry. + void set_comment(std::string_view comment); + + /// Get the list of conditions configured for the package. + const std::vector & get_conditions() const { return conditions; } + + /// Add a new condition for the package + void add_condition(VersionlockCondition && condition); + + /// Get list of errors found during parsing the entry from configuration file. + const std::vector & get_errors() const { return errors; } + + /// Converts the package configuration to string usable for printing. + /// @param with_errors Include also error messages for invalid entries + std::string to_string(bool with_errors, bool with_comment) const; + +private: + bool valid; + std::string name; + std::string comment; + std::vector conditions{}; + std::vector errors{}; +}; + + +/// Class contains parsed versionlock configuration file. +/// @since 5.1.13 +class VersionlockConfig { +public: + /// Get list of configured versionlock entries. + std::vector & get_packages() { return packages; } + + /// Save configuration to the file specified in the constructor. + void save(); + +private: + friend class PackageSack; + + /// Creates an instance of `VersionlockConfig` specifying the config file + /// to read. + /// @param path Path to versionlock configuration file. + VersionlockConfig(const std::filesystem::path & path); + + std::filesystem::path path; + std::vector packages{}; +}; + +} // namespace libdnf5::rpm + +#endif // LIBDNF5_RPM_VERSIONLOCK_CONFIG_HPP diff --git a/libdnf5/base/log_event.cpp b/libdnf5/base/log_event.cpp index 04dbf09f1..972a52aaf 100644 --- a/libdnf5/base/log_event.cpp +++ b/libdnf5/base/log_event.cpp @@ -112,6 +112,8 @@ std::string LogEvent::to_string( return ret.append(utils::sformat(_("Argument '{}' matches only source packages."), *spec)); case GoalProblem::EXCLUDED: return ret.append(utils::sformat(_("Argument '{}' matches only excluded packages."), *spec)); + case GoalProblem::EXCLUDED_VERSIONLOCK: + return ret.append(utils::sformat(_("Argument '{}' matches only packages excluded by versionlock."), *spec)); case GoalProblem::HINT_ICASE: if (additional_data.size() != 1) { throw std::invalid_argument("Incorrect number of elements for HINT_ICASE"); diff --git a/libdnf5/base/transaction.cpp b/libdnf5/base/transaction.cpp index 94a693c97..c05415f32 100644 --- a/libdnf5/base/transaction.cpp +++ b/libdnf5/base/transaction.cpp @@ -238,16 +238,29 @@ GoalProblem Transaction::Impl::report_not_found( log_level); return GoalProblem::ONLY_SRC; } - // TODO(jmracek) make difference between regular excludes and modular excludes - add_resolve_log( - action, - GoalProblem::EXCLUDED, - settings, - libdnf5::transaction::TransactionItemType::PACKAGE, - pkg_spec, - {}, - log_level); - return GoalProblem::EXCLUDED; + query.filter_versionlock(); + if (query.empty()) { + add_resolve_log( + action, + GoalProblem::EXCLUDED_VERSIONLOCK, + settings, + libdnf5::transaction::TransactionItemType::PACKAGE, + pkg_spec, + {}, + log_level); + return GoalProblem::EXCLUDED_VERSIONLOCK; + } else { + // TODO(jmracek) make difference between regular excludes and modular excludes + add_resolve_log( + action, + GoalProblem::EXCLUDED, + settings, + libdnf5::transaction::TransactionItemType::PACKAGE, + pkg_spec, + {}, + log_level); + return GoalProblem::EXCLUDED; + } } void Transaction::Impl::add_resolve_log( diff --git a/libdnf5/rpm/package_query.cpp b/libdnf5/rpm/package_query.cpp index 897f14e4f..c00b4de00 100644 --- a/libdnf5/rpm/package_query.cpp +++ b/libdnf5/rpm/package_query.cpp @@ -632,6 +632,10 @@ inline static bool cmp_eq(int cmp) { return cmp == 0; } +inline static bool cmp_neq(int cmp) { + return cmp != 0; +} + inline static bool cmp_gte(int cmp) { return cmp >= 0; } @@ -676,6 +680,9 @@ void PackageQuery::filter_evr(const std::vector & patterns, libdnf5 case libdnf5::sack::QueryCmp::EQ: filter_evr_internal(pool, patterns, *p_impl); break; + case libdnf5::sack::QueryCmp::NEQ: + filter_evr_internal(pool, patterns, *p_impl); + break; default: libdnf_throw_assert_unsupported_query_cmp_type(cmp_type); } @@ -2907,4 +2914,10 @@ void PackageQuery::filter_reboot_suggested() { *p_impl |= core_packages; } +void PackageQuery::filter_versionlock() { + auto sack = p_impl->base->get_rpm_package_sack(); + auto versionlock_excludes = sack->get_versionlock_excludes(); + *p_impl -= *versionlock_excludes.p_impl; +} + } // namespace libdnf5::rpm diff --git a/libdnf5/rpm/package_sack.cpp b/libdnf5/rpm/package_sack.cpp index c9ba471f9..c5c91d018 100644 --- a/libdnf5/rpm/package_sack.cpp +++ b/libdnf5/rpm/package_sack.cpp @@ -25,7 +25,10 @@ along with libdnf. If not, see . #include "solv/solv_map.hpp" #include "libdnf5/common/exception.hpp" +#include "libdnf5/common/sack/query_cmp.hpp" +#include "libdnf5/conf/const.hpp" #include "libdnf5/rpm/package_query.hpp" +#include "libdnf5/rpm/versionlock_config.hpp" #include @@ -89,6 +92,67 @@ void PackageSack::Impl::make_provides_ready() { get_rpm_pool(base).swap_considered_map(original_considered_map); } +void PackageSack::Impl::load_versionlock_excludes() { + PackageSet locked_set(base); + PackageQuery base_query(base, PackageQuery::ExcludeFlags::IGNORE_EXCLUDES); + base_query.filter_available(); + + auto vl_conf = get_versionlock_config(); + std::unordered_set locked_names; + for (const auto & vl_pkg : vl_conf.get_packages()) { + if (!vl_pkg.is_valid()) { + // skip misconfigured package (e.g. missing name) + // TODO(mblaha): log skipped entries? + continue; + } + PackageQuery pkg_query(base_query); + pkg_query.filter_name({vl_pkg.get_name()}, libdnf5::sack::QueryCmp::GLOB); + for (const auto & pkg : pkg_query) { + locked_names.emplace(pkg.get_name()); + } + for (const auto & vl_condition : vl_pkg.get_conditions()) { + if (!vl_condition.is_valid()) { + // skip misconfigured version condition + // TODO(mblaha): log skipped entries? + continue; + } + switch (vl_condition.get_key()) { + case VersionlockCondition::Keys::EPOCH: + pkg_query.filter_epoch( + std::vector{std::stoul(vl_condition.get_value())}, + vl_condition.get_comparator()); + break; + case VersionlockCondition::Keys::EVR: + pkg_query.filter_evr({vl_condition.get_value()}, vl_condition.get_comparator()); + break; + case VersionlockCondition::Keys::ARCH: + pkg_query.filter_arch({vl_condition.get_value()}, vl_condition.get_comparator()); + break; + } + } + locked_set |= pkg_query; + } + + if (locked_names.empty()) { + return; + } + + PackageQuery versionlock_excludes(base_query); + versionlock_excludes.filter_name(std::vector(locked_names.begin(), locked_names.end())); + versionlock_excludes -= locked_set; + + // exclude also anything that obsoletes the locked versions + PackageQuery obsoletes_query(base_query); + obsoletes_query.filter_obsoletes(locked_set); + // leave out obsoleters that are also part of locked versions. Otherwise the + // obsoleter package would not be installable at all. + obsoletes_query -= locked_set; + versionlock_excludes |= obsoletes_query; + + set_versionlock_excludes(versionlock_excludes); +} + + void PackageSack::Impl::load_config_excludes_includes(bool only_main) { considered_uptodate = false; @@ -191,6 +255,8 @@ void PackageSack::Impl::load_config_excludes_includes(bool only_main) { config_excludes.reset(new libdnf5::solv::SolvMap(0)); *config_excludes = *excludes.p_impl; } + + load_versionlock_excludes(); } const PackageSet PackageSack::Impl::get_user_excludes() { @@ -299,6 +365,50 @@ void PackageSack::Impl::clear_module_excludes() { considered_uptodate = false; } +VersionlockConfig PackageSack::Impl::get_versionlock_config() const { + const auto & config = base->get_config(); + std::filesystem::path conf_file_path{libdnf5::VERSIONLOCK_CONF_FILENAME}; + if (!config.get_use_host_config_option().get_value()) { + const std::filesystem::path installroot_path{config.get_installroot_option().get_value()}; + conf_file_path = installroot_path / conf_file_path.relative_path(); + } + return VersionlockConfig(conf_file_path); +} + +const PackageSet PackageSack::Impl::get_versionlock_excludes() { + if (versionlock_excludes) { + return PackageSet(base, *versionlock_excludes); + } else { + return PackageSet(base); + } +} + +void PackageSack::Impl::add_versionlock_excludes(const PackageSet & excludes) { + if (versionlock_excludes) { + *versionlock_excludes |= *excludes.p_impl; + considered_uptodate = false; + } else { + set_versionlock_excludes(excludes); + } +} + +void PackageSack::Impl::remove_versionlock_excludes(const PackageSet & excludes) { + if (versionlock_excludes) { + *versionlock_excludes -= *excludes.p_impl; + considered_uptodate = false; + } +} + +void PackageSack::Impl::set_versionlock_excludes(const PackageSet & excludes) { + versionlock_excludes.reset(new libdnf5::solv::SolvMap(*excludes.p_impl)); + considered_uptodate = false; +} + +void PackageSack::Impl::clear_versionlock_excludes() { + versionlock_excludes.reset(); + considered_uptodate = false; +} + std::optional PackageSack::Impl::compute_considered_map( libdnf5::sack::ExcludeFlags flags) const { if ((static_cast(flags & libdnf5::sack::ExcludeFlags::IGNORE_REGULAR_CONFIG_EXCLUDES) || @@ -306,7 +416,8 @@ std::optional PackageSack::Impl::compute_considered_map( (static_cast(flags & libdnf5::sack::ExcludeFlags::IGNORE_REGULAR_USER_EXCLUDES) || (!user_excludes && !user_includes)) && (static_cast(flags & libdnf5::sack::ExcludeFlags::USE_DISABLED_REPOSITORIES) || !repo_excludes) && - (static_cast(flags & libdnf5::sack::ExcludeFlags::IGNORE_MODULAR_EXCLUDES) || !module_excludes)) { + (static_cast(flags & libdnf5::sack::ExcludeFlags::IGNORE_MODULAR_EXCLUDES) || !module_excludes) && + (static_cast(flags & libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK) || !versionlock_excludes)) { return {}; } @@ -314,7 +425,7 @@ std::optional PackageSack::Impl::compute_considered_map( libdnf5::solv::SolvMap considered(pool.get_nsolvables()); - // considered = (all - module_excludes - repo_excludes - config_excludes - user_excludes) and + // considered = (all - module_excludes - repo_excludes - config_excludes - user_excludes - versionlock) and // (config_includes + user_includes + all_from_repos_not_using_includes) considered.set_all(); @@ -326,6 +437,10 @@ std::optional PackageSack::Impl::compute_considered_map( considered -= *repo_excludes; } + if (!static_cast(flags & libdnf5::sack::ExcludeFlags::IGNORE_VERSIONLOCK) && versionlock_excludes) { + considered -= *versionlock_excludes; + } + if (!static_cast(flags & libdnf5::sack::ExcludeFlags::IGNORE_REGULAR_EXCLUDES)) { std::unique_ptr pkg_includes; if (!static_cast(flags & libdnf5::sack::ExcludeFlags::IGNORE_REGULAR_CONFIG_EXCLUDES)) { @@ -450,6 +565,30 @@ void PackageSack::clear_user_includes() { p_impl->clear_user_includes(); } +VersionlockConfig PackageSack::get_versionlock_config() const { + return p_impl->get_versionlock_config(); +} + +const PackageSet PackageSack::get_versionlock_excludes() { + return p_impl->get_versionlock_excludes(); +} + +void PackageSack::add_versionlock_excludes(const PackageSet & excludes) { + p_impl->add_versionlock_excludes(excludes); +} + +void PackageSack::remove_versionlock_excludes(const PackageSet & excludes) { + p_impl->remove_versionlock_excludes(excludes); +} + +void PackageSack::set_versionlock_excludes(const PackageSet & excludes) { + p_impl->set_versionlock_excludes(excludes); +} + +void PackageSack::clear_versionlock_excludes() { + p_impl->clear_versionlock_excludes(); +} + static libdnf5::rpm::PackageQuery running_kernel_check_path(const libdnf5::BaseWeakPtr & base, const std::string & fn) { auto & logger = *base->get_logger(); if (access(fn.c_str(), F_OK)) { diff --git a/libdnf5/rpm/package_sack_impl.hpp b/libdnf5/rpm/package_sack_impl.hpp index 5077272f9..91e6cbf60 100644 --- a/libdnf5/rpm/package_sack_impl.hpp +++ b/libdnf5/rpm/package_sack_impl.hpp @@ -91,6 +91,9 @@ class PackageSack::Impl { // TODO(jrohel): Is param `only_main` needed? Used in DNF4 with commandline repo. void load_config_excludes_includes(bool only_main = false); + /// Load versionlock excludes from the config file. + void load_versionlock_excludes(); + const PackageSet get_user_excludes(); void add_user_excludes(const PackageSet & excludes); void remove_user_excludes(const PackageSet & excludes); @@ -109,6 +112,13 @@ class PackageSack::Impl { void set_module_excludes(const PackageSet & excludes); void clear_module_excludes(); + VersionlockConfig get_versionlock_config() const; + const PackageSet get_versionlock_excludes(); + void add_versionlock_excludes(const PackageSet & excludes); + void remove_versionlock_excludes(const PackageSet & excludes); + void set_versionlock_excludes(const PackageSet & excludes); + void clear_versionlock_excludes(); + /// Computes considered map. /// If there are no excluded packages, the considered map may not be present in the return value. std::optional compute_considered_map(libdnf5::sack::ExcludeFlags flags) const; @@ -137,6 +147,9 @@ class PackageSack::Impl { std::unique_ptr module_excludes; // packages excluded by modularity + // packages excluded by versionlock feature + std::unique_ptr versionlock_excludes; // packages excluded by versionlock + bool considered_uptodate = true; std::vector cached_sorted_solvables; diff --git a/libdnf5/rpm/versionlock_config.cpp b/libdnf5/rpm/versionlock_config.cpp new file mode 100644 index 000000000..c60d6bf7d --- /dev/null +++ b/libdnf5/rpm/versionlock_config.cpp @@ -0,0 +1,267 @@ +/* +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 Lesser General Public License as published by +the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with libdnf. If not, see . +*/ + +#include "libdnf5/rpm/versionlock_config.hpp" + +#include "utils/string.hpp" + +#include "libdnf5/common/exception.hpp" +#include "libdnf5/common/sack/query_cmp.hpp" +#include "libdnf5/utils/fs/file.hpp" + +#include +#include +#include + +#include +#include + +namespace { +// supported config file version +const std::string_view CONFIG_FILE_VERSION = "1.0"; +} // namespace + + +namespace toml { + +template <> +struct from { + static libdnf5::rpm::VersionlockCondition from_toml(const toml::value & val) { + auto key = toml::find_or(val, "key", ""); + auto comparator = toml::find_or(val, "comparator", ""); + auto value = toml::find_or(val, "value", ""); + + libdnf5::rpm::VersionlockCondition condition(key, comparator, value); + + return condition; + } +}; + +template <> +struct into { + static toml::value into_toml(const libdnf5::rpm::VersionlockCondition & condition) { + toml::value res; + + res["key"] = condition.get_key_str(); + res["comparator"] = condition.get_comparator_str(); + res["value"] = condition.get_value(); + + return res; + } +}; + +template <> +struct from { + static libdnf5::rpm::VersionlockPackage from_toml(const toml::value & val) { + auto name = toml::find_or(val, "name", ""); + libdnf5::rpm::VersionlockPackage package( + name, toml::find_or>(val, "conditions", {})); + auto comment = toml::find_or(val, "comment", ""); + if (!comment.empty()) { + package.set_comment(comment); + } + + return package; + } +}; + +template <> +struct into { + static toml::value into_toml(const libdnf5::rpm::VersionlockPackage & package) { + toml::value res; + + res["name"] = package.get_name(); + auto comment = package.get_comment(); + if (!comment.empty()) { + res["comment"] = package.get_comment(); + } + res["conditions"] = package.get_conditions(); + + return res; + } +}; + +} // namespace toml + + +namespace libdnf5::rpm { + +const std::map VersionlockCondition::VALID_COMPARATORS = { + {"=", libdnf5::sack::QueryCmp::EQ}, + {"==", libdnf5::sack::QueryCmp::EQ}, + {"<", libdnf5::sack::QueryCmp::LT}, + {"<=", libdnf5::sack::QueryCmp::LTE}, + {">", libdnf5::sack::QueryCmp::GT}, + {">=", libdnf5::sack::QueryCmp::GTE}, + {"<>", libdnf5::sack::QueryCmp::NEQ}, + {"!=", libdnf5::sack::QueryCmp::NEQ}, +}; + +VersionlockCondition::VersionlockCondition( + const std::string & key_str, const std::string & comparator_str, const std::string & value) + : valid(true), + key_str(key_str), + comparator_str(comparator_str), + value(value) { + // check that condition key is present and valid + if (key_str == "epoch") { + key = Keys::EPOCH; + } else if (key_str == "evr") { + key = Keys::EVR; + } else if (key_str == "arch") { + key = Keys::ARCH; + } else { + valid = false; + if (key_str.empty()) { + errors.emplace_back("missing condition key"); + } else { + errors.emplace_back(fmt::format("invalid condition key \"{}\"", key_str)); + } + } + + // check that condition comparison operator is present and valid + if (VALID_COMPARATORS.contains(comparator_str)) { + comparator = VALID_COMPARATORS.at(comparator_str); + } else { + valid = false; + if (comparator_str.empty()) { + errors.emplace_back("missing condition comparison operator"); + } else { + errors.emplace_back(fmt::format("invalid condition comparison operator \"{}\"", comparator_str)); + } + } + + // check that condition value is present + if (value.empty()) { + valid = false; + errors.emplace_back("missing condition value"); + } + + if (valid) { + // additional checks for specific keys + switch (key) { + case Keys::EPOCH: + // the epoch condition requires a valid integer as a value + try { + std::stoul(value); + } catch (...) { + valid = false; + errors.emplace_back("epoch condition value needs to be an unsigned integer"); + } + break; + case Keys::ARCH: + if (comparator != libdnf5::sack::QueryCmp::EQ && comparator != libdnf5::sack::QueryCmp::NEQ) { + valid = false; + errors.emplace_back("\"arch\" condition only supports \"=\" and \"!=\" comparison operators"); + } + default: + break; + } + } +} + +std::string VersionlockCondition::to_string(bool with_errors) const { + std::string str = fmt::format("{} {} {}", key_str, comparator_str, value); + if (!valid && with_errors) { + str += fmt::format(" # {}", utils::string::join(errors, ", ")); + } + return str; +} + + +VersionlockPackage::VersionlockPackage( + std::string_view name, std::vector && conditions) + : valid(true), + name(name), + conditions(std::move(conditions)) { + // check that package name is present + if (name.empty()) { + valid = false; + errors.emplace_back("missing package name"); + } + // package without any condition doesn't lock anything + if (this->conditions.empty()) { + valid = false; + errors.emplace_back("missing package conditions"); + } +} + +void VersionlockPackage::set_comment(std::string_view comment) { + this->comment = comment; +} + +void VersionlockPackage::add_condition(VersionlockCondition && condition) { + conditions.emplace_back(std::move(condition)); +} + +std::string VersionlockPackage::to_string(bool with_errors, bool with_comment) const { + std::string str; + if (with_comment && !comment.empty()) { + str += fmt::format("# {}\n", comment); + } + str += fmt::format("Package name: {}", name); + if (!valid and with_errors) { + str += fmt::format(" # entry is invalid: {}", utils::string::join(errors, ", ")); + } + for (const auto & cond : conditions) { + str += "\n"; + str += cond.to_string(with_errors); + } + return str; +} + + +VersionlockConfig::VersionlockConfig(const std::filesystem::path & path) : path(path) { + if (!std::filesystem::exists(path)) { + return; + } + + auto toml_value = toml::parse(this->path); + + if (!toml_value.contains("version")) { + // TODO(mblaha) Log unversioned versionlock file? + return; + } else if (toml::find(toml_value, "version") != CONFIG_FILE_VERSION) { + // TODO(mblaha) Log unsupported versionlock file version? + return; + } + + packages = toml::find_or>(toml_value, "packages", {}); +} + +template +static toml::value make_top_value(const std::string & key, const T & value) { + return toml::value({{key, value}, {"version", CONFIG_FILE_VERSION}}); +} + +static std::string toml_format(const toml::value & value) { + return toml::format(value); +} + +void VersionlockConfig::save() { + std::error_code ecode; + std::filesystem::create_directories(path.parent_path(), ecode); + if (ecode) { + throw FileSystemError(errno, path, M_("{}"), ecode.message()); + } + + utils::fs::File(path, "w").write(toml_format(make_top_value("packages", packages))); +} + +} // namespace libdnf5::rpm