diff --git a/dnf5-plugins/CMakeLists.txt b/dnf5-plugins/CMakeLists.txt
index 433002674..ce8504276 100644
--- a/dnf5-plugins/CMakeLists.txt
+++ b/dnf5-plugins/CMakeLists.txt
@@ -13,3 +13,4 @@ add_subdirectory("config-manager_plugin")
add_subdirectory("copr_plugin")
add_subdirectory("needs_restarting_plugin")
add_subdirectory("repoclosure_plugin")
+add_subdirectory("system_upgrade_plugin")
diff --git a/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp b/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp
index 0ea4cbeb0..d9e806332 100644
--- a/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp
+++ b/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp
@@ -46,4 +46,4 @@ class NeedsRestartingCommand : public Command {
} // namespace dnf5
-#endif // DNF5_COMMANDS_CHANGELOG_HPP
+#endif // DNF5_COMMANDS_NEEDS_RESTARTING_HPP
diff --git a/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt b/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt
new file mode 100644
index 000000000..bd6519526
--- /dev/null
+++ b/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(SYSTEMD_UNIT_DIR /usr/lib/systemd/system)
+
+# set gettext domain for translations
+add_definitions(-DGETTEXT_DOMAIN=\"dnf5_cmd_system_upgrade\")
+
+file(GLOB SYSTEM_UPGRADE_SOURCES *.cpp)
+add_library(system_upgrade_cmd_plugin MODULE ${SYSTEM_UPGRADE_SOURCES})
+
+# disable the 'lib' prefix in order to create system_upgrade_cmd_plugin.so
+set_target_properties(system_upgrade_cmd_plugin PROPERTIES PREFIX "")
+
+pkg_check_modules(SDBUS_CPP REQUIRED sdbus-c++)
+pkg_check_modules(LIBSYSTEMD REQUIRED libsystemd)
+
+target_link_libraries(system_upgrade_cmd_plugin PRIVATE dnf5 libdnf5 libdnf5-cli ${SDBUS_CPP_LIBRARIES} ${LIBSYSTEMD_LIBRARIES})
+
+install(FILES "dnf5-system-upgrade.service" DESTINATION ${SYSTEMD_UNIT_DIR})
+install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
+ ${SYSTEMD_UNIT_DIR}/dnf5-system-upgrade.service
+ ${SYSTEMD_UNIT_DIR}/system-update.target.wants/dnf5-system-upgrade.service)"
+)
+install(TARGETS system_upgrade_cmd_plugin LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins/)
diff --git a/dnf5-plugins/system_upgrade_plugin/dnf5-system-upgrade.service b/dnf5-plugins/system_upgrade_plugin/dnf5-system-upgrade.service
new file mode 100644
index 000000000..a846ad5bf
--- /dev/null
+++ b/dnf5-plugins/system_upgrade_plugin/dnf5-system-upgrade.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=System Upgrade using DNF 5
+ConditionPathExists=/system-update
+Documentation=https://www.freedesktop.org/wiki/Software/systemd/SystemUpdates
+
+DefaultDependencies=no
+Requires=sysinit.target
+After=sysinit.target systemd-journald.socket system-update-pre.target
+Before=poweroff.target reboot.target shutdown.target system-update.target
+
+[Service]
+# We are done when the script exits, not before
+Type=oneshot
+# Upgrade output goes to journal and on-screen.
+StandardOutput=journal+console
+ExecStart=/usr/bin/dnf5 system-upgrade upgrade
+
+[Install]
+WantedBy=system-update.target
diff --git a/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp b/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp
new file mode 100644
index 000000000..95c6bc62e
--- /dev/null
+++ b/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp
@@ -0,0 +1,672 @@
+/*
+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 "system_upgrade.hpp"
+
+#include "utils/string.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+using namespace libdnf5::cli;
+
+const std::string & ID_TO_IDENTIFY_BOOTS = OFFLINE_STARTED_ID;
+
+OfflineTransactionState::OfflineTransactionState(std::filesystem::path path) : path(std::move(path)) {
+ read();
+}
+void OfflineTransactionState::read() {
+ try {
+ const auto & value = toml::parse(path);
+ data = toml::find(value, STATE_HEADER);
+ if (data.state_version != STATE_VERSION) {
+ throw libdnf5::RuntimeError(M_("incompatible version of state data"));
+ }
+ } catch (const std::exception & ex) {
+ read_exception = std::current_exception();
+ data = OfflineTransactionStateData{};
+ }
+}
+void OfflineTransactionState::write() {
+ auto file = libdnf5::utils::fs::File(path, "w");
+ file.write(toml::format(toml::value{{STATE_HEADER, data}}));
+ file.close();
+}
+
+int call(const std::string & command, const std::vector & args) {
+ std::vector c_args;
+ c_args.emplace_back(const_cast(command.c_str()));
+ for (const auto & arg : args) {
+ c_args.emplace_back(const_cast(arg.c_str()));
+ }
+ c_args.emplace_back(nullptr);
+
+ const auto pid = fork();
+ if (pid == -1) {
+ return -1;
+ }
+ if (pid == 0) {
+ int rc = execvp(command.c_str(), c_args.data());
+ exit(rc == 0 ? 0 : -1);
+ } else {
+ int status;
+ int rc = waitpid(pid, &status, 0);
+ if (rc == -1) {
+ return -1;
+ }
+ if (WIFEXITED(status)) {
+ return WEXITSTATUS(status);
+ }
+ if (WIFSIGNALED(status)) {
+ return 128 + WTERMSIG(status);
+ }
+ return -1;
+ }
+}
+
+namespace dnf5 {
+
+/// Helper for displaying messages with Plymouth
+///
+/// Derived from DNF 4 system-upgrade PlymouthOutput implementation. Filters
+/// duplicate calls, and stops calling the plymouth binary if we fail to
+/// contact it.
+class PlymouthOutput {
+public:
+ bool ping() { return plymouth({"ping"}); }
+ bool set_mode() { return plymouth({"change-mode", "--system-upgrade"}); }
+ bool message(const std::string & message) {
+ if (last_message.has_value() && message == last_message) {
+ plymouth({"hide-message", "--text", last_message.value()});
+ }
+ last_message = message;
+ return plymouth({"display-message", "--text", message});
+ }
+ bool progress(const int percentage) {
+ return plymouth({"system-update", "--progress", std::to_string(percentage)});
+ }
+
+private:
+ bool alive = true;
+ std::map> last_subcommand_args;
+ std::optional last_message;
+ bool plymouth(const std::vector & args) {
+ const auto & command = args.at(0);
+ const auto & last_args = last_subcommand_args.find(command);
+
+ bool dupe_cmd = (last_args != last_subcommand_args.end() && args == last_args->second);
+ if ((alive && !dupe_cmd) || command == "--ping") {
+ alive = call(PATH_TO_PLYMOUTH, args) == 0;
+ last_subcommand_args[command] = args;
+ }
+ return alive;
+ }
+};
+
+/// Extend RpmTransCB to also display messages with Plymouth
+class PlymouthTransCB : public RpmTransCB {
+public:
+ PlymouthTransCB(PlymouthOutput plymouth) : plymouth(std::move(plymouth)) {}
+ void elem_progress(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ [[maybe_unused]] uint64_t amount,
+ [[maybe_unused]] uint64_t total) override {
+ RpmTransCB::elem_progress(item, amount, total);
+
+ plymouth.progress(static_cast(100 * static_cast(amount) / static_cast(total)));
+
+ std::string action;
+ switch (item.get_action()) {
+ case libdnf5::transaction::TransactionItemAction::UPGRADE:
+ action = "Upgrading";
+ break;
+ case libdnf5::transaction::TransactionItemAction::DOWNGRADE:
+ action = "Downgrading";
+ break;
+ case libdnf5::transaction::TransactionItemAction::REINSTALL:
+ action = "Reinstalling";
+ break;
+ case libdnf5::transaction::TransactionItemAction::INSTALL:
+ action = "Installing";
+ break;
+ case libdnf5::transaction::TransactionItemAction::REMOVE:
+ action = "Removing";
+ break;
+ case libdnf5::transaction::TransactionItemAction::REPLACED:
+ action = "Replacing";
+ break;
+ case libdnf5::transaction::TransactionItemAction::REASON_CHANGE:
+ case libdnf5::transaction::TransactionItemAction::ENABLE:
+ case libdnf5::transaction::TransactionItemAction::DISABLE:
+ case libdnf5::transaction::TransactionItemAction::RESET:
+ throw std::logic_error(fmt::format(
+ "Unexpected action in TransactionPackage: {}",
+ static_cast>(
+ item.get_action())));
+ break;
+ }
+ const auto & message = fmt::format("[{}/{}] {} {}...", amount, total, action, item.get_package().get_name());
+ plymouth.message(message);
+ }
+
+private:
+ PlymouthOutput plymouth;
+};
+
+void SystemUpgradeCommand::pre_configure() {
+ throw_missing_command();
+}
+
+void SystemUpgradeCommand::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 SystemUpgradeCommand::set_argument_parser() {
+ get_argument_parser_command()->set_description("Prepare system for upgrade to a new release");
+}
+
+void SystemUpgradeCommand::register_subcommands() {
+ register_subcommand(std::make_unique(get_context()));
+ register_subcommand(std::make_unique(get_context()));
+ register_subcommand(std::make_unique(get_context()));
+ register_subcommand(std::make_unique(get_context()));
+ register_subcommand(std::make_unique(get_context()));
+}
+
+SystemUpgradeSubcommand::SystemUpgradeSubcommand(Context & context, const std::string & name)
+ : Command(context, name),
+ datadir(std::filesystem::path{libdnf5::SYSTEM_STATE_DIR} / "system-upgrade"),
+ state(datadir / "state.toml") {}
+
+void SystemUpgradeSubcommand::set_argument_parser() {
+ auto & ctx = get_context();
+ auto & parser = ctx.get_argument_parser();
+ auto & cmd = *get_argument_parser_command();
+
+ cachedir =
+ dynamic_cast(parser.add_init_value(std::make_unique(datadir)));
+
+ auto * download_dir_arg = parser.add_new_named_arg("downloaddir");
+ download_dir_arg->set_long_name("downloaddir");
+ download_dir_arg->set_description("Redirect download of packages to provided ");
+ download_dir_arg->link_value(cachedir);
+ cmd.register_named_arg(download_dir_arg);
+}
+
+void SystemUpgradeSubcommand::configure() {
+ auto & ctx = get_context();
+ const std::filesystem::path installroot{ctx.base.get_config().get_installroot_option().get_value()};
+ magic_symlink = installroot / "system-update";
+
+ system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot);
+ target_releasever = ctx.base.get_vars()->get_value("releasever");
+}
+
+void SystemUpgradeSubcommand::log_status(const std::string & message, const std::string & message_id) const {
+ const auto & version = get_application_version();
+ const std::string & version_string = fmt::format("{}.{}.{}", version.major, version.minor, version.micro);
+ sd_journal_send(
+ "MESSAGE=%s",
+ message.c_str(),
+ "MESSAGE_ID=%s",
+ message_id.c_str(),
+ "SYSTEM_RELEASEVER=%s",
+ get_system_releasever().c_str(),
+ "TARGET_RELEASEVER=%s",
+ get_target_releasever().c_str(),
+ "DNF_VERSION=%s",
+ version_string.c_str(),
+ NULL);
+}
+
+void SystemUpgradeDownloadCommand::set_argument_parser() {
+ SystemUpgradeSubcommand::set_argument_parser();
+
+ auto & ctx = get_context();
+ auto & parser = ctx.get_argument_parser();
+ auto & cmd = *get_argument_parser_command();
+
+ cmd.set_description("Downloads everything needed to upgrade to a new release");
+
+ no_downgrade =
+ dynamic_cast(parser.add_init_value(std::make_unique(true)));
+
+ auto * no_downgrade_arg = parser.add_new_named_arg("no-downgrade");
+ no_downgrade_arg->set_long_name("no-downgrade");
+ no_downgrade_arg->set_description(
+ "Do not install packages from the new release if they are older than what is currently installed");
+ no_downgrade_arg->link_value(no_downgrade);
+
+ cmd.register_named_arg(no_downgrade_arg);
+}
+
+void SystemUpgradeDownloadCommand::configure() {
+ SystemUpgradeSubcommand::configure();
+
+ auto & ctx = get_context();
+
+ // Check --releasever
+ if (get_target_releasever() == get_system_releasever()) {
+ throw libdnf5::cli::CommandExitError(1, M_("Need a --releasever greater than the current system version."));
+ }
+
+ ctx.set_load_system_repo(true);
+ ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED);
+
+ ctx.base.get_config().get_cachedir_option().set(
+ libdnf5::Option::Priority::PLUGINDEFAULT, get_cachedir()->get_value());
+}
+
+void SystemUpgradeDownloadCommand::run() {
+ auto & ctx = get_context();
+
+ const auto & goal = std::make_unique(ctx.base);
+
+ auto & conf = ctx.base.get_config();
+ conf.get_tsflags_option().set(libdnf5::Option::Priority::RUNTIME, "test");
+
+ if (no_downgrade->get_value()) {
+ goal->add_rpm_upgrade();
+ } else {
+ goal->add_rpm_distro_sync();
+ }
+
+ auto transaction = goal->resolve();
+ if (transaction.get_problems() != libdnf5::GoalProblem::NO_PROBLEM) {
+ throw libdnf5::cli::GoalResolveError(transaction);
+ }
+
+ ctx.download_and_run(transaction);
+
+ if (transaction.get_transaction_packages_count() == 0) {
+ throw libdnf5::cli::CommandExitError(
+ 1, M_("The system-upgrade transaction is empty; your system is already up-to-date."));
+ }
+
+ const std::string json = transaction.serialize();
+ const auto & transaction_json_path = std::filesystem::path(get_datadir()) / "system-upgrade-transaction.json";
+ auto transaction_json_file = libdnf5::utils::fs::File(transaction_json_path, "w");
+
+ transaction_json_file.write(json);
+ transaction_json_file.close();
+
+ auto state = get_state();
+ state.get_data().cachedir = get_cachedir()->get_value();
+ state.get_data().system_releasever = get_system_releasever();
+ state.get_data().target_releasever = get_target_releasever();
+ state.get_data().status = STATUS_DOWNLOAD_COMPLETE;
+ libdnf5::repo::RepoQuery enabled_repos(ctx.base);
+ enabled_repos.filter_enabled(true);
+ enabled_repos.filter_type(libdnf5::repo::Repo::Type::SYSTEM, libdnf5::sack::QueryCmp::NEQ);
+ for (const auto & repo : enabled_repos) {
+ state.get_data().enabled_repos.emplace_back(repo->get_id());
+ }
+ libdnf5::repo::RepoQuery disabled_repos(ctx.base);
+ disabled_repos.filter_enabled(false);
+ disabled_repos.filter_type(libdnf5::repo::Repo::Type::SYSTEM, libdnf5::sack::QueryCmp::NEQ);
+ for (const auto & repo : disabled_repos) {
+ state.get_data().disabled_repos.emplace_back(repo->get_id());
+ }
+ state.write();
+
+ std::cout << "Download complete! Use `dnf5 system-upgrade reboot` to start the upgrade." << std::endl
+ << "To cancel the upgrade and delete the downloaded upgrade files, use `dnf5 system-upgrade clean`."
+ << std::endl;
+}
+
+void check_state(const OfflineTransactionState & state) {
+ const auto & read_exception = state.get_read_exception();
+ if (read_exception != nullptr) {
+ try {
+ std::rethrow_exception(read_exception);
+ } catch (const std::exception & ex) {
+ const std::string message{ex.what()};
+ throw libdnf5::cli::CommandExitError(
+ 1, M_("Error reading state: {}. Rerun `dnf5 system-upgrade download [OPTIONS]`."), message);
+ }
+ }
+}
+
+void reboot(Context & ctx, bool poweroff = false) {
+ const std::string systemd_destination_name{"org.freedesktop.systemd1"};
+ const std::string systemd_object_path{"/org/freedesktop/systemd1"};
+ const std::string systemd_manager_interface{"org.freedesktop.systemd1.Manager"};
+
+ if (std::getenv("DNF_SYSTEM_UPGRADE_NO_REBOOT")) {
+ ctx.base.get_logger()->info("Reboot turned off, not rebooting.");
+ return;
+ }
+
+ poweroff = !poweroff;
+ std::unique_ptr connection;
+ try {
+ connection = sdbus::createSystemBusConnection();
+ } catch (const sdbus::Error & ex) {
+ const std::string error_message{ex.what()};
+ throw libdnf5::cli::CommandExitError(1, M_("Couldn't connect to D-Bus: {}"), error_message);
+ }
+ auto proxy = sdbus::createProxy(systemd_destination_name, systemd_object_path);
+ if (poweroff) {
+ proxy->callMethod("Poweroff").onInterface(systemd_manager_interface);
+ } else {
+ proxy->callMethod("Reboot").onInterface(systemd_manager_interface);
+ }
+}
+
+void clean(Context & ctx, const std::filesystem::path & datadir) {
+ ctx.base.get_logger()->info("Cleaning up downloaded data...");
+
+ for (const auto & entry : std::filesystem::directory_iterator(datadir)) {
+ std::filesystem::remove_all(entry.path());
+ }
+}
+
+void SystemUpgradeRebootCommand::set_argument_parser() {
+ SystemUpgradeSubcommand::set_argument_parser();
+
+ auto & ctx = get_context();
+ auto & parser = ctx.get_argument_parser();
+ auto & cmd = *get_argument_parser_command();
+
+ cmd.set_description("Prepare the system to perform the upgrade and reboots to start the upgrade.");
+
+ poweroff_after =
+ dynamic_cast(parser.add_init_value(std::make_unique(true)));
+
+ auto * poweroff_after_arg = parser.add_new_named_arg("poweroff");
+ poweroff_after_arg->set_long_name("poweroff");
+ poweroff_after_arg->set_description("Power off the system after the operation is complete");
+ poweroff_after_arg->link_value(poweroff_after);
+
+ cmd.register_named_arg(poweroff_after_arg);
+}
+
+void SystemUpgradeRebootCommand::run() {
+ auto & ctx = get_context();
+
+ auto state = get_state();
+ check_state(state);
+ if (state.get_data().status != STATUS_DOWNLOAD_COMPLETE) {
+ throw libdnf5::cli::CommandExitError(1, M_("system is not ready for upgrade"));
+ }
+ if (std::filesystem::is_symlink(get_magic_symlink())) {
+ throw libdnf5::cli::CommandExitError(1, M_("upgrade is already scheduled"));
+ }
+
+ if (!std::filesystem::is_directory(get_datadir())) {
+ throw libdnf5::cli::CommandExitError(1, M_("data directory {} does not exist"), get_datadir().string());
+ }
+
+ std::cout << "The system will now reboot to upgrade to release version " << state.get_data().target_releasever
+ << ". Do you want to continue?" << std::endl;
+ if (!libdnf5::cli::utils::userconfirm::userconfirm(ctx.base.get_config())) {
+ return;
+ }
+
+ std::filesystem::create_symlink(get_datadir(), get_magic_symlink());
+
+ state.get_data().status = STATUS_READY;
+ state.get_data().poweroff_after = poweroff_after->get_value();
+ state.write();
+
+ reboot(get_context(), poweroff_after->get_value());
+}
+
+void SystemUpgradeUpgradeCommand::set_argument_parser() {}
+
+void SystemUpgradeUpgradeCommand::configure() {
+ SystemUpgradeSubcommand::configure();
+ auto & ctx = get_context();
+
+ ctx.set_load_system_repo(true);
+ ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED);
+
+ auto state = get_state();
+ check_state(state);
+
+ // Set same set of enabled/disabled repos used during `system-upgrade download`
+ for (const auto & repo_id : state.get_data().enabled_repos) {
+ ctx.setopts.emplace_back(repo_id + ".enabled", "1");
+ }
+ for (const auto & repo_id : state.get_data().disabled_repos) {
+ ctx.setopts.emplace_back(repo_id + ".disabled", "1");
+ }
+
+ // Don't try to refresh metadata, we are offline
+ ctx.base.get_config().get_cacheonly_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, "all");
+ // Don't ask any questions
+ ctx.base.get_config().get_assumeyes_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, true);
+ // Override `assumeno` too since it takes priority over `assumeyes`
+ ctx.base.get_config().get_assumeno_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, false);
+ // Upgrade operation already removes all element that must be removed.
+ // Additional removal could trigger unwanted changes in transaction.
+ ctx.base.get_config().get_clean_requirements_on_remove_option().set(
+ libdnf5::Option::Priority::PLUGINDEFAULT, false);
+ ctx.base.get_config().get_install_weak_deps_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, false);
+}
+
+void SystemUpgradeUpgradeCommand::run() {
+ auto & ctx = get_context();
+
+ log_status("Starting offline transaction. This will take a while.", OFFLINE_STARTED_ID);
+
+ if (!std::filesystem::is_symlink(get_magic_symlink())) {
+ throw libdnf5::cli::CommandExitError(0, M_("Trigger file does not exist. Exiting."));
+ }
+
+ const auto & symlinked_path = std::filesystem::read_symlink(get_magic_symlink());
+ if (symlinked_path != get_datadir()) {
+ throw libdnf5::cli::CommandExitError(0, M_("Another upgrade tool is running. Exiting."));
+ }
+
+ std::filesystem::remove(get_magic_symlink());
+
+ auto state = get_state();
+ if (state.get_data().status != STATUS_READY) {
+ throw libdnf5::cli::CommandExitError(1, M_("Use `dnf5 system-upgrade reboot` to begin the upgrade."));
+ }
+
+ const auto & transaction_json_path = std::filesystem::path(get_datadir()) / "system-upgrade-transaction.json";
+ auto transaction_json_file = libdnf5::utils::fs::File(transaction_json_path, "r");
+
+ const auto & json = transaction_json_file.read();
+ transaction_json_file.close();
+
+ const auto & goal = std::make_unique(ctx.base);
+
+ auto settings = libdnf5::GoalJobSettings();
+ goal->add_serialized_transaction(json, settings);
+
+ auto transaction = goal->resolve();
+ if (transaction.get_problems() != libdnf5::GoalProblem::NO_PROBLEM) {
+ throw libdnf5::cli::GoalResolveError(transaction);
+ }
+
+ PlymouthOutput plymouth;
+ auto callbacks = std::make_unique(plymouth);
+ /* callbacks->get_multi_progress_bar()->set_total_num_of_bars(num_of_actions); */
+ transaction.set_callbacks(std::move(callbacks));
+
+ const auto result = transaction.run();
+ std::cout << std::endl;
+ if (result != libdnf5::base::Transaction::TransactionRunResult::SUCCESS) {
+ std::cerr << "Transaction failed: " << libdnf5::base::Transaction::transaction_result_to_string(result)
+ << std::endl;
+ for (auto const & entry : transaction.get_gpg_signature_problems()) {
+ std::cerr << entry << std::endl;
+ }
+ for (auto & problem : transaction.get_transaction_problems()) {
+ std::cerr << " - " << problem << std::endl;
+ }
+ throw libdnf5::cli::SilentCommandExitError(1);
+ }
+
+ for (auto const & entry : transaction.get_gpg_signature_problems()) {
+ std::cerr << entry << std::endl;
+ }
+
+ std::string upgrade_complete_message;
+ if (state.get_data().poweroff_after) {
+ upgrade_complete_message = "Upgrade complete! Cleaning up and powering off...";
+ } else {
+ upgrade_complete_message = "Upgrade complete! Cleaning up and rebooting...";
+ }
+ plymouth.message(upgrade_complete_message);
+ log_status(upgrade_complete_message, OFFLINE_STARTED_ID);
+
+ // If the upgrade succeeded, remove downloaded data
+ clean(ctx, get_datadir());
+
+ reboot(ctx, state.get_data().poweroff_after);
+}
+
+void SystemUpgradeCleanCommand::set_argument_parser() {
+ SystemUpgradeSubcommand::set_argument_parser();
+}
+
+void SystemUpgradeCleanCommand::run() {
+ auto & ctx = get_context();
+ clean(ctx, get_datadir());
+}
+
+struct BootEntry {
+ std::string boot_id;
+ std::string timestamp;
+ std::string system_releasever;
+ std::string target_releasever;
+};
+
+std::string get_journal_field(sd_journal * journal, const std::string & field) {
+ const char * data = nullptr;
+ size_t length = 0;
+ auto rc = sd_journal_get_data(journal, field.c_str(), reinterpret_cast(&data), &length);
+ if (rc < 0 || data == nullptr) {
+ return "";
+ }
+ const auto prefix_length = field.length() + 1;
+ return std::string{data + prefix_length, length - prefix_length};
+}
+
+std::vector find_boots(const std::string & message_id) {
+ std::vector boots{};
+
+ sd_journal * journal = nullptr;
+ auto rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY);
+ if (rc < 0) {
+ throw libdnf5::RuntimeError(M_("Error reading journal: {}"), std::string{std::strerror(-rc)});
+ }
+
+ const auto & uid_filter_string = fmt::format("MESSAGE_ID={}", message_id);
+ rc = sd_journal_add_match(journal, uid_filter_string.c_str(), 0);
+ if (rc < 0) {
+ throw libdnf5::RuntimeError(M_("Add journal match failed: {}"), std::string{std::strerror(-rc)});
+ }
+ SD_JOURNAL_FOREACH(journal) {
+ uint64_t usec = 0;
+ rc = sd_journal_get_realtime_usec(journal, &usec);
+ auto sec = usec / (1000 * 1000);
+ boots.emplace_back(BootEntry{
+ get_journal_field(journal, "_BOOT_ID"),
+ libdnf5::utils::string::format_epoch(sec),
+ get_journal_field(journal, "SYSTEM_RELEASEVER"),
+ get_journal_field(journal, "TARGET_RELEASEVER"),
+ });
+ }
+
+ return boots;
+}
+
+void list_logs() {
+ const auto & boot_entries = find_boots(ID_TO_IDENTIFY_BOOTS);
+
+ std::cout << "The following boots appear to contain upgrade logs:" << std::endl;
+ for (size_t index = 0; index < boot_entries.size(); index += 1) {
+ const auto & entry = boot_entries[index];
+ std::cout << fmt::format(
+ "{} / {}: {} {}→{}",
+ index + 1,
+ entry.boot_id,
+ entry.timestamp,
+ entry.system_releasever,
+ entry.target_releasever)
+ << std::endl;
+ }
+ if (boot_entries.empty()) {
+ std::cout << "No logs were found." << std::endl;
+ }
+}
+
+void show_log(size_t boot_index) {
+ const auto & boot_entries = find_boots(ID_TO_IDENTIFY_BOOTS);
+ if (boot_index >= boot_entries.size()) {
+ throw libdnf5::cli::CommandExitError(1, M_("Cannot find logs with this index."));
+ }
+
+ const auto & boot_id = boot_entries[boot_index].boot_id;
+ const auto rc = call(PATH_TO_JOURNALCTL, {"--boot", boot_id});
+
+ if (rc != 0 && rc != 141) {
+ throw libdnf5::cli::CommandExitError(1, M_("Unable to match systemd journal entry."));
+ }
+}
+
+void SystemUpgradeLogCommand::set_argument_parser() {
+ SystemUpgradeSubcommand::set_argument_parser();
+
+ auto & ctx = get_context();
+ auto & parser = ctx.get_argument_parser();
+ auto & cmd = *get_argument_parser_command();
+
+ cmd.set_description("Show logs from past upgrades");
+
+ number = dynamic_cast(parser.add_init_value(std::make_unique("")));
+
+ auto * number_arg = parser.add_new_named_arg("number");
+ number_arg->set_long_name("number");
+ number_arg->set_has_value(true);
+
+ number_arg->set_description("Which log to show. Run without any arguments to get a list of available logs.");
+ number_arg->link_value(number);
+ cmd.register_named_arg(number_arg);
+}
+
+void SystemUpgradeLogCommand::run() {
+ if (number->get_value().empty()) {
+ list_logs();
+ } else {
+ std::string number_string{number->get_value()};
+ show_log(std::stoul(number_string) - 1);
+ }
+}
+
+} // namespace dnf5
diff --git a/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp b/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp
new file mode 100644
index 000000000..c976b1bc9
--- /dev/null
+++ b/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp
@@ -0,0 +1,166 @@
+/*
+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_SYSTEM_UPGRADE_HPP
+#define DNF5_COMMANDS_SYSTEM_UPGRADE_HPP
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+const std::filesystem::path PATH_TO_PLYMOUTH{"/usr/bin/plymouth"};
+const std::filesystem::path PATH_TO_JOURNALCTL{"/usr/bin/journalctl"};
+const std::string OFFLINE_STARTED_ID{"3e0a5636d16b4ca4bbe5321d06c6aa62"};
+const std::string OFFLINE_FINISHED_ID{"8cec00a1566f4d3594f116450395f06c"};
+
+const std::string STATUS_DOWNLOAD_INCOMPLETE{"download-incomplete"};
+const std::string STATUS_DOWNLOAD_COMPLETE{"download-complete"};
+const std::string STATUS_READY{"ready"};
+const std::string STATUS_UPGRADE_INCOMPLETE{"upgrade-incomplete"};
+
+const int STATE_VERSION = 0;
+const std::string STATE_HEADER{"offline-transaction-state"};
+
+struct OfflineTransactionStateData {
+ int state_version = STATE_VERSION;
+ std::string status = STATUS_DOWNLOAD_INCOMPLETE;
+ std::string cachedir;
+ std::string target_releasever;
+ std::string system_releasever;
+ bool poweroff_after = false;
+ std::vector enabled_repos;
+ std::vector disabled_repos;
+};
+
+TOML11_DEFINE_CONVERSION_NON_INTRUSIVE(
+ OfflineTransactionStateData,
+ state_version,
+ status,
+ cachedir,
+ target_releasever,
+ system_releasever,
+ poweroff_after,
+ enabled_repos,
+ disabled_repos)
+
+class OfflineTransactionState {
+public:
+ void write();
+ OfflineTransactionState(std::filesystem::path path);
+ OfflineTransactionStateData & get_data() { return data; };
+ const std::exception_ptr & get_read_exception() const { return read_exception; };
+
+private:
+ void read();
+ std::exception_ptr read_exception;
+ std::filesystem::path path;
+ OfflineTransactionStateData data;
+};
+
+const std::filesystem::path DEFAULT_DATADIR{std::filesystem::path(libdnf5::SYSTEM_STATE_DIR) / "system-upgrade"};
+
+namespace dnf5 {
+
+class SystemUpgradeCommand : public Command {
+public:
+ explicit SystemUpgradeCommand(Context & context) : Command(context, "system-upgrade") {}
+ void set_parent_command() override;
+ void set_argument_parser() override;
+ void register_subcommands() override;
+ void pre_configure() override;
+};
+
+class SystemUpgradeSubcommand : public Command {
+public:
+ explicit SystemUpgradeSubcommand(Context & context, const std::string & name);
+ void set_argument_parser() override;
+ void configure() override;
+
+protected:
+ libdnf5::OptionPath * get_cachedir() const { return cachedir; };
+ std::filesystem::path get_datadir() const { return datadir; };
+ std::filesystem::path get_magic_symlink() const { return magic_symlink; };
+ OfflineTransactionState get_state() const { return state; };
+ std::string get_system_releasever() const { return system_releasever; };
+ std::string get_target_releasever() const { return target_releasever; };
+ void log_status(const std::string & message, const std::string & message_id) const;
+
+private:
+ libdnf5::OptionPath * cachedir{nullptr};
+ std::filesystem::path datadir{DEFAULT_DATADIR};
+ std::filesystem::path magic_symlink;
+ OfflineTransactionState state;
+ std::string target_releasever;
+ std::string system_releasever;
+};
+
+class SystemUpgradeDownloadCommand : public SystemUpgradeSubcommand {
+public:
+ explicit SystemUpgradeDownloadCommand(Context & context) : SystemUpgradeSubcommand{context, "download"} {}
+ void set_argument_parser() override;
+ void configure() override;
+ void run() override;
+
+private:
+ libdnf5::OptionBool * no_downgrade{nullptr};
+ libdnf5::OptionBool * distro_sync{nullptr};
+ int state_version;
+};
+
+class SystemUpgradeRebootCommand : public SystemUpgradeSubcommand {
+public:
+ explicit SystemUpgradeRebootCommand(Context & context) : SystemUpgradeSubcommand(context, "reboot") {}
+ void set_argument_parser() override;
+ void run() override;
+
+private:
+ libdnf5::OptionBool * poweroff_after{nullptr};
+};
+
+class SystemUpgradeUpgradeCommand : public SystemUpgradeSubcommand {
+public:
+ explicit SystemUpgradeUpgradeCommand(Context & context) : SystemUpgradeSubcommand(context, "upgrade") {}
+ void set_argument_parser() override;
+ void configure() override;
+ void run() override;
+};
+
+class SystemUpgradeCleanCommand : public SystemUpgradeSubcommand {
+public:
+ explicit SystemUpgradeCleanCommand(Context & context) : SystemUpgradeSubcommand(context, "clean") {}
+ void set_argument_parser() override;
+ void run() override;
+};
+
+class SystemUpgradeLogCommand : public SystemUpgradeSubcommand {
+public:
+ explicit SystemUpgradeLogCommand(Context & context) : SystemUpgradeSubcommand(context, "log") {}
+ void set_argument_parser() override;
+ void run() override;
+
+private:
+ libdnf5::OptionString * number{nullptr};
+};
+
+} // namespace dnf5
+
+#endif // DNF5_COMMANDS_CHANGELOG_HPP
diff --git a/dnf5-plugins/system_upgrade_plugin/system_upgrade_cmd_plugin.cpp b/dnf5-plugins/system_upgrade_plugin/system_upgrade_cmd_plugin.cpp
new file mode 100644
index 000000000..7c5970e50
--- /dev/null
+++ b/dnf5-plugins/system_upgrade_plugin/system_upgrade_cmd_plugin.cpp
@@ -0,0 +1,72 @@
+#include "system_upgrade.hpp"
+
+#include
+
+using namespace dnf5;
+
+namespace {
+
+constexpr const char * PLUGIN_NAME{"system_upgrade"};
+constexpr PluginVersion PLUGIN_VERSION{.major = 1, .minor = 0, .micro = 0};
+
+constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr};
+constexpr const char * attrs_value[]{"Evan Goode", "egoode@redhat.com", "system_upgrade command."};
+
+class SystemUpgradeCmdPlugin : public IPlugin {
+public:
+ using IPlugin::IPlugin;
+
+ PluginAPIVersion get_api_version() const noexcept override { return PLUGIN_API_VERSION; }
+
+ const char * get_name() const noexcept override { return PLUGIN_NAME; }
+
+ PluginVersion get_version() const noexcept override { return PLUGIN_VERSION; }
+
+ const char * const * get_attributes() const noexcept override { return attrs; }
+
+ const char * get_attribute(const char * attribute) const noexcept override {
+ for (size_t i = 0; attrs[i]; ++i) {
+ if (std::strcmp(attribute, attrs[i]) == 0) {
+ return attrs_value[i];
+ }
+ }
+ return nullptr;
+ }
+
+ std::vector> create_commands() override;
+
+ void finish() noexcept override {}
+};
+
+
+std::vector> SystemUpgradeCmdPlugin::create_commands() {
+ std::vector> commands;
+ commands.push_back(std::make_unique(get_context()));
+ return commands;
+}
+
+
+} // namespace
+
+
+PluginAPIVersion dnf5_plugin_get_api_version(void) {
+ return PLUGIN_API_VERSION;
+}
+
+const char * dnf5_plugin_get_name(void) {
+ return PLUGIN_NAME;
+}
+
+PluginVersion dnf5_plugin_get_version(void) {
+ return PLUGIN_VERSION;
+}
+
+IPlugin * dnf5_plugin_new_instance([[maybe_unused]] ApplicationVersion application_version, Context & context) try {
+ return new SystemUpgradeCmdPlugin(context);
+} catch (...) {
+ return nullptr;
+}
+
+void dnf5_plugin_delete_instance(IPlugin * plugin_object) {
+ delete plugin_object;
+}
diff --git a/dnf5.spec b/dnf5.spec
index fd02ef56a..2b97eb810 100644
--- a/dnf5.spec
+++ b/dnf5.spec
@@ -174,6 +174,7 @@ BuildRequires: pkgconfig(smartcols)
%if %{with dnf5_plugins}
BuildRequires: libcurl-devel >= 7.62.0
+BuildRequires: systemd-devel
%endif
%if %{with dnf5daemon_server}
@@ -653,17 +654,20 @@ Provides: dnf5-command(config-manager)
Provides: dnf5-command(copr)
Provides: dnf5-command(needs-restarting)
Provides: dnf5-command(repoclosure)
+Provides: dnf5-command(system-upgrade)
%description -n dnf5-plugins
Core DNF5 plugins that enhance dnf5 with builddep, changelog,
config-manager, copr, and repoclosure commands.
%files -n dnf5-plugins -f dnf5-plugin-builddep.lang -f dnf5-plugin-changelog.lang -f dnf5-plugin-config-manager.lang -f dnf5-plugin-copr.lang -f dnf5-plugin-needs-restarting.lang -f dnf5-plugin-repoclosure.lang
+%config %{_unitdir}/dnf5-system-upgrade.service
%{_libdir}/dnf5/plugins/*.so
%{_mandir}/man8/dnf5-builddep.8.*
%{_mandir}/man8/dnf5-copr.8.*
%{_mandir}/man8/dnf5-needs-restarting.8.*
%{_mandir}/man8/dnf5-repoclosure.8.*
+# %{_mandir}/man8/dnf5-system-upgrade.8.*
%endif
diff --git a/dnf5/context.cpp b/dnf5/context.cpp
index 942e69a59..43ba888f6 100644
--- a/dnf5/context.cpp
+++ b/dnf5/context.cpp
@@ -184,230 +184,211 @@ void Context::load_repos(bool load_system) {
print_info("Repositories loaded.");
}
-namespace {
+RpmTransCB::RpmTransCB() {
+ multi_progress_bar.set_total_bar_visible_limit(libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT);
+}
-class RpmTransCB : public libdnf5::rpm::TransactionCallbacks {
-public:
- RpmTransCB() {
- multi_progress_bar.set_total_bar_visible_limit(
- libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT);
+RpmTransCB::~RpmTransCB() {
+ if (active_progress_bar && active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) {
+ active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS);
}
-
- ~RpmTransCB() {
- if (active_progress_bar &&
- active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) {
- active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS);
- }
- if (active_progress_bar) {
- multi_progress_bar.print();
- }
- }
-
- libdnf5::cli::progressbar::MultiProgressBar * get_multi_progress_bar() { return &multi_progress_bar; }
-
- void install_progress(
- [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
- uint64_t amount,
- [[maybe_unused]] uint64_t total) override {
- active_progress_bar->set_ticks(static_cast(amount));
- if (is_time_to_print()) {
- multi_progress_bar.print();
- }
+ if (active_progress_bar) {
+ multi_progress_bar.print();
}
+}
- void install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override {
- const char * msg{nullptr};
- switch (item.get_action()) {
- case libdnf5::transaction::TransactionItemAction::UPGRADE:
- msg = "Upgrading ";
- break;
- case libdnf5::transaction::TransactionItemAction::DOWNGRADE:
- msg = "Downgrading ";
- break;
- case libdnf5::transaction::TransactionItemAction::REINSTALL:
- msg = "Reinstalling ";
- break;
- case libdnf5::transaction::TransactionItemAction::INSTALL:
- case libdnf5::transaction::TransactionItemAction::REMOVE:
- case libdnf5::transaction::TransactionItemAction::REPLACED:
- break;
- case libdnf5::transaction::TransactionItemAction::REASON_CHANGE:
- case libdnf5::transaction::TransactionItemAction::ENABLE:
- case libdnf5::transaction::TransactionItemAction::DISABLE:
- case libdnf5::transaction::TransactionItemAction::RESET:
- throw std::logic_error(fmt::format(
- "Unexpected action in TransactionPackage: {}",
- static_cast>(
- item.get_action())));
- }
- if (!msg) {
- msg = "Installing ";
- }
- new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra());
- }
+libdnf5::cli::progressbar::MultiProgressBar * RpmTransCB::get_multi_progress_bar() {
+ return &multi_progress_bar;
+}
- void install_stop(
- [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
- [[maybe_unused]] uint64_t amount,
- [[maybe_unused]] uint64_t total) override {
+void RpmTransCB::install_progress(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, uint64_t amount, [[maybe_unused]] uint64_t total) {
+ active_progress_bar->set_ticks(static_cast(amount));
+ if (is_time_to_print()) {
multi_progress_bar.print();
}
+}
- void transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override {
- active_progress_bar->set_ticks(static_cast(amount));
- if (is_time_to_print()) {
- multi_progress_bar.print();
- }
- }
+void RpmTransCB::install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) {
+ const char * msg{nullptr};
+ switch (item.get_action()) {
+ case libdnf5::transaction::TransactionItemAction::UPGRADE:
+ msg = "Upgrading ";
+ break;
+ case libdnf5::transaction::TransactionItemAction::DOWNGRADE:
+ msg = "Downgrading ";
+ break;
+ case libdnf5::transaction::TransactionItemAction::REINSTALL:
+ msg = "Reinstalling ";
+ break;
+ case libdnf5::transaction::TransactionItemAction::INSTALL:
+ case libdnf5::transaction::TransactionItemAction::REMOVE:
+ case libdnf5::transaction::TransactionItemAction::REPLACED:
+ break;
+ case libdnf5::transaction::TransactionItemAction::REASON_CHANGE:
+ case libdnf5::transaction::TransactionItemAction::ENABLE:
+ case libdnf5::transaction::TransactionItemAction::DISABLE:
+ case libdnf5::transaction::TransactionItemAction::RESET:
+ throw std::logic_error(fmt::format(
+ "Unexpected action in TransactionPackage: {}",
+ static_cast>(
+ item.get_action())));
+ }
+ if (!msg) {
+ msg = "Installing ";
+ }
+ new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra());
+}
- void transaction_start(uint64_t total) override {
- new_progress_bar(static_cast(total), "Prepare transaction");
- }
+void RpmTransCB::install_stop(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ [[maybe_unused]] uint64_t amount,
+ [[maybe_unused]] uint64_t total) {
+ multi_progress_bar.print();
+}
- void transaction_stop([[maybe_unused]] uint64_t total) override {
- active_progress_bar->set_ticks(static_cast(total));
+void RpmTransCB::transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) {
+ active_progress_bar->set_ticks(static_cast(amount));
+ if (is_time_to_print()) {
multi_progress_bar.print();
}
+}
- void uninstall_progress(
- [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
- uint64_t amount,
- [[maybe_unused]] uint64_t total) override {
- active_progress_bar->set_ticks(static_cast(amount));
- if (is_time_to_print()) {
- multi_progress_bar.print();
- }
- }
+void RpmTransCB::transaction_start(uint64_t total) {
+ new_progress_bar(static_cast(total), "Prepare transaction");
+}
- void uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override {
- const char * msg{nullptr};
- if (item.get_action() == libdnf5::transaction::TransactionItemAction::REMOVE ||
- item.get_action() == libdnf5::transaction::TransactionItemAction::REPLACED) {
- msg = "Erasing ";
- }
- if (!msg) {
- msg = "Cleanup ";
- }
- new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra());
- }
+void RpmTransCB::transaction_stop([[maybe_unused]] uint64_t total) {
+ active_progress_bar->set_ticks(static_cast(total));
+ multi_progress_bar.print();
+}
- void uninstall_stop(
- [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
- [[maybe_unused]] uint64_t amount,
- [[maybe_unused]] uint64_t total) override {
+void RpmTransCB::uninstall_progress(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, uint64_t amount, [[maybe_unused]] uint64_t total) {
+ active_progress_bar->set_ticks(static_cast(amount));
+ if (is_time_to_print()) {
multi_progress_bar.print();
}
+}
-
- void unpack_error(const libdnf5::rpm::TransactionItem & item) override {
- active_progress_bar->add_message(
- libdnf5::cli::progressbar::MessageType::ERROR, "Unpack error: " + item.get_package().get_full_nevra());
- active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR);
- multi_progress_bar.print();
+void RpmTransCB::uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) {
+ const char * msg{nullptr};
+ if (item.get_action() == libdnf5::transaction::TransactionItemAction::REMOVE ||
+ item.get_action() == libdnf5::transaction::TransactionItemAction::REPLACED) {
+ msg = "Erasing ";
}
-
- void cpio_error(const libdnf5::rpm::TransactionItem & item) override {
- active_progress_bar->add_message(
- libdnf5::cli::progressbar::MessageType::ERROR, "Cpio error: " + item.get_package().get_full_nevra());
- active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR);
- multi_progress_bar.print();
+ if (!msg) {
+ msg = "Cleanup ";
}
+ new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra());
+}
- void script_error(
- [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
- libdnf5::rpm::Nevra nevra,
- libdnf5::rpm::TransactionCallbacks::ScriptType type,
- uint64_t return_code) override {
- active_progress_bar->add_message(
- libdnf5::cli::progressbar::MessageType::ERROR,
- fmt::format(
- "Error in {} scriptlet: {} return code {}",
- script_type_to_string(type),
- to_full_nevra_string(nevra),
- return_code));
- multi_progress_bar.print();
- }
+void RpmTransCB::uninstall_stop(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ [[maybe_unused]] uint64_t amount,
+ [[maybe_unused]] uint64_t total) {
+ multi_progress_bar.print();
+}
- void script_start(
- [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
- libdnf5::rpm::Nevra nevra,
- libdnf5::rpm::TransactionCallbacks::ScriptType type) override {
- active_progress_bar->add_message(
- libdnf5::cli::progressbar::MessageType::INFO,
- fmt::format("Running {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra)));
- multi_progress_bar.print();
- }
- void script_stop(
- [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
- libdnf5::rpm::Nevra nevra,
- libdnf5::rpm::TransactionCallbacks::ScriptType type,
- [[maybe_unused]] uint64_t return_code) override {
- active_progress_bar->add_message(
- libdnf5::cli::progressbar::MessageType::INFO,
- fmt::format("Stop {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra)));
- multi_progress_bar.print();
- }
+void RpmTransCB::unpack_error(const libdnf5::rpm::TransactionItem & item) {
+ active_progress_bar->add_message(
+ libdnf5::cli::progressbar::MessageType::ERROR, "Unpack error: " + item.get_package().get_full_nevra());
+ active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR);
+ multi_progress_bar.print();
+}
- void elem_progress(
- [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
- [[maybe_unused]] uint64_t amount,
- [[maybe_unused]] uint64_t total) override {
- //std::cout << "Element progress: " << header.get_full_nevra() << " " << amount << '/' << total << std::endl;
- }
+void RpmTransCB::cpio_error(const libdnf5::rpm::TransactionItem & item) {
+ active_progress_bar->add_message(
+ libdnf5::cli::progressbar::MessageType::ERROR, "Cpio error: " + item.get_package().get_full_nevra());
+ active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR);
+ multi_progress_bar.print();
+}
- void verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override {
- active_progress_bar->set_ticks(static_cast(amount));
- if (is_time_to_print()) {
- multi_progress_bar.print();
- }
- }
+void RpmTransCB::script_error(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
+ libdnf5::rpm::Nevra nevra,
+ libdnf5::rpm::TransactionCallbacks::ScriptType type,
+ uint64_t return_code) {
+ active_progress_bar->add_message(
+ libdnf5::cli::progressbar::MessageType::ERROR,
+ fmt::format(
+ "Error in {} scriptlet: {} return code {}",
+ script_type_to_string(type),
+ to_full_nevra_string(nevra),
+ return_code));
+ multi_progress_bar.print();
+}
- void verify_start([[maybe_unused]] uint64_t total) override {
- new_progress_bar(static_cast(total), "Verify package files");
- }
+void RpmTransCB::script_start(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
+ libdnf5::rpm::Nevra nevra,
+ libdnf5::rpm::TransactionCallbacks::ScriptType type) {
+ active_progress_bar->add_message(
+ libdnf5::cli::progressbar::MessageType::INFO,
+ fmt::format("Running {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra)));
+ multi_progress_bar.print();
+}
+
+void RpmTransCB::script_stop(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
+ libdnf5::rpm::Nevra nevra,
+ libdnf5::rpm::TransactionCallbacks::ScriptType type,
+ [[maybe_unused]] uint64_t return_code) {
+ active_progress_bar->add_message(
+ libdnf5::cli::progressbar::MessageType::INFO,
+ fmt::format("Stop {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra)));
+ multi_progress_bar.print();
+}
+
+void RpmTransCB::elem_progress(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ [[maybe_unused]] uint64_t amount,
+ [[maybe_unused]] uint64_t total) {
+ //std::cout << "Element progress: " << header.get_full_nevra() << " " << amount << '/' << total << std::endl;
+}
- void verify_stop([[maybe_unused]] uint64_t total) override {
- active_progress_bar->set_ticks(static_cast(total));
+void RpmTransCB::verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) {
+ active_progress_bar->set_ticks(static_cast(amount));
+ if (is_time_to_print()) {
multi_progress_bar.print();
}
+}
-private:
- void new_progress_bar(int64_t total, const std::string & descr) {
- if (active_progress_bar &&
- active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) {
- active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS);
- }
- auto progress_bar =
- std::make_unique(static_cast(total), descr);
- progress_bar->set_auto_finish(false);
- progress_bar->start();
- active_progress_bar = progress_bar.get();
- multi_progress_bar.add_bar(std::move(progress_bar));
- }
-
- static bool is_time_to_print() {
- auto now = std::chrono::steady_clock::now();
- auto delta = now - prev_print_time;
- auto ms = std::chrono::duration_cast(delta).count();
- if (ms > 100) {
- // 100ms equals to 10 FPS and that seems to be smooth enough
- prev_print_time = now;
- return true;
- }
- return false;
- }
+void RpmTransCB::verify_start([[maybe_unused]] uint64_t total) {
+ new_progress_bar(static_cast(total), "Verify package files");
+}
- static std::chrono::time_point prev_print_time;
+void RpmTransCB::verify_stop([[maybe_unused]] uint64_t total) {
+ active_progress_bar->set_ticks(static_cast(total));
+ multi_progress_bar.print();
+}
+void RpmTransCB::new_progress_bar(int64_t total, const std::string & descr) {
+ if (active_progress_bar && active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) {
+ active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS);
+ }
+ auto progress_bar =
+ std::make_unique(static_cast(total), descr);
+ progress_bar->set_auto_finish(false);
+ progress_bar->start();
+ active_progress_bar = progress_bar.get();
+ multi_progress_bar.add_bar(std::move(progress_bar));
+}
- libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar;
- libdnf5::cli::progressbar::DownloadProgressBar * active_progress_bar{nullptr};
-};
+bool RpmTransCB::is_time_to_print() {
+ auto now = std::chrono::steady_clock::now();
+ auto delta = now - prev_print_time;
+ auto ms = std::chrono::duration_cast(delta).count();
+ if (ms > 100) {
+ // 100ms equals to 10 FPS and that seems to be smooth enough
+ prev_print_time = now;
+ return true;
+ }
+ return false;
+}
std::chrono::time_point RpmTransCB::prev_print_time = std::chrono::steady_clock::now();
-} // namespace
-
void Context::download_and_run(libdnf5::base::Transaction & transaction) {
transaction.download();
diff --git a/dnf5/include/dnf5/context.hpp b/dnf5/include/dnf5/context.hpp
index df430e4aa..f3021ba63 100644
--- a/dnf5/include/dnf5/context.hpp
+++ b/dnf5/include/dnf5/context.hpp
@@ -23,6 +23,7 @@ along with libdnf. If not, see .
#include "version.hpp"
#include
+#include
#include
#include
#include
@@ -183,6 +184,85 @@ class RpmTransactionItem : public libdnf5::rpm::TransactionItem {
Actions action;
};
+class RpmTransCB : public libdnf5::rpm::TransactionCallbacks {
+public:
+ RpmTransCB();
+ ~RpmTransCB();
+ libdnf5::cli::progressbar::MultiProgressBar * get_multi_progress_bar();
+
+ void install_progress(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ uint64_t amount,
+ [[maybe_unused]] uint64_t total) override;
+
+ void install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override;
+ void install_stop(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ [[maybe_unused]] uint64_t amount,
+ [[maybe_unused]] uint64_t total) override;
+
+ void transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override;
+
+ void transaction_start(uint64_t total) override;
+
+ void transaction_stop([[maybe_unused]] uint64_t total) override;
+
+ void uninstall_progress(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ uint64_t amount,
+ [[maybe_unused]] uint64_t total) override;
+
+ void uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override;
+
+ void uninstall_stop(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ [[maybe_unused]] uint64_t amount,
+ [[maybe_unused]] uint64_t total) override;
+
+
+ void unpack_error(const libdnf5::rpm::TransactionItem & item) override;
+
+ void cpio_error(const libdnf5::rpm::TransactionItem & item) override;
+
+ void script_error(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
+ libdnf5::rpm::Nevra nevra,
+ libdnf5::rpm::TransactionCallbacks::ScriptType type,
+ uint64_t return_code) override;
+
+ void script_start(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
+ libdnf5::rpm::Nevra nevra,
+ libdnf5::rpm::TransactionCallbacks::ScriptType type) override;
+
+ void script_stop(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem * item,
+ libdnf5::rpm::Nevra nevra,
+ libdnf5::rpm::TransactionCallbacks::ScriptType type,
+ [[maybe_unused]] uint64_t return_code) override;
+
+ void elem_progress(
+ [[maybe_unused]] const libdnf5::rpm::TransactionItem & item,
+ [[maybe_unused]] uint64_t amount,
+ [[maybe_unused]] uint64_t total) override;
+
+ void verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override;
+
+ void verify_start([[maybe_unused]] uint64_t total) override;
+
+ void verify_stop([[maybe_unused]] uint64_t total) override;
+
+private:
+ void new_progress_bar(int64_t total, const std::string & descr);
+
+ static bool is_time_to_print();
+
+ static std::chrono::time_point prev_print_time;
+
+ libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar;
+ libdnf5::cli::progressbar::DownloadProgressBar * active_progress_bar{nullptr};
+};
+
void run_transaction(libdnf5::rpm::Transaction & transaction);
/// Returns the names of matching packages and paths of matching package file names and directories.