Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package libdnf-plugin-txnupd for
openSUSE:Factory checked in at 2026-11-10 16:29:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libdnf-plugin-txnupd (Old)
and /work/SRC/openSUSE:Factory/.libdnf-plugin-txnupd.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "libdnf-plugin-txnupd"
Tue Nov 10 16:29:32 2026 rev:5 rq:1396201 version:0.4.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/libdnf-plugin-txnupd/libdnf-plugin-txnupd.changes
2026-06-17 16:17:00.211759885 +0200
+++
/work/SRC/openSUSE:Factory/.libdnf-plugin-txnupd.new.3759290/libdnf-plugin-txnupd.changes
2026-11-10 16:29:34.000650352 +0100
@@ -1,0 +2,12 @@
+Fri Nov 6 20:50:24 UTC 2026 - Shawn Dunn <[email protected]>
+
+- Update to 0.4.0:
+ * Fix linking with libtukit
+ * Finalize snapshots after libdnf5 closes rpmdb
+ * Log finish() messages in logger
+ * Report snapshot create failures, instead of aborting
+ * apply-live: Sync /etc state before applying snapshot
+ * Drop version information from manpage
+- Drop 3add4c39b5506f8bfafd1daca3bf6165e55c5e18.patch
+
+-------------------------------------------------------------------
Old:
----
3add4c39b5506f8bfafd1daca3bf6165e55c5e18.patch
libdnf-plugin-txnupd-0.3.0.tar.gz
New:
----
libdnf-plugin-txnupd-0.4.0.tar.gz
----------(Old B)----------
Old: * Drop version information from manpage
- Drop 3add4c39b5506f8bfafd1daca3bf6165e55c5e18.patch
----------(Old E)----------
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ libdnf-plugin-txnupd.spec ++++++
--- /var/tmp/diff_new_pack.sBd0Hr/_old 2026-11-10 16:29:35.159698833 +0100
+++ /var/tmp/diff_new_pack.sBd0Hr/_new 2026-11-10 16:29:35.161698917 +0100
@@ -2,6 +2,7 @@
# spec file for package libdnf-plugin-txnupd
#
# Copyright (c) 2026 Neal Gompa.
+# Copyright (c) 2026 Shawn W Dunn <[email protected]>
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -22,16 +23,13 @@
Name: libdnf-plugin-txnupd
-Version: 0.3.0
+Version: 0.4.0
Release: 0
Summary: Plugin for libdnf5 to implement transactional updates
License: LGPL-2.1-or-later
URL:
https://gitlab.com/VelocityLimitless/Projects/libdnf-plugin-txnupd
Source0: %{url}/-/archive/%{version}/%{name}-%{version}.tar.gz
-# Backports from upstream
-Patch0001: %{url}/-/commit/3add4c39b5506f8bfafd1daca3bf6165e55c5e18.patch
-
BuildRequires: cmake
BuildRequires: make
BuildRequires: gcc-c++ >= 13
++++++ libdnf-plugin-txnupd-0.3.0.tar.gz -> libdnf-plugin-txnupd-0.4.0.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/libdnf-plugin-txnupd-0.3.0/CMakeLists.txt
new/libdnf-plugin-txnupd-0.4.0/CMakeLists.txt
--- old/libdnf-plugin-txnupd-0.3.0/CMakeLists.txt 2026-05-31
19:55:44.000000000 +0200
+++ new/libdnf-plugin-txnupd-0.4.0/CMakeLists.txt 2026-11-06
16:30:26.000000000 +0100
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.18)
project(libdnf-plugin-txnupd
- VERSION 0.3.0
+ VERSION 0.4.0
LANGUAGES CXX
)
@@ -52,6 +52,7 @@
# Shared utility code (PUBLIC includes so plugin subdirs inherit the path)
add_library(txnupd_util STATIC src/txnupd_util.cpp)
target_include_directories(txnupd_util PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
+target_link_libraries(txnupd_util PUBLIC PkgConfig::TUKIT)
add_subdirectory(src/libdnf5-plugin)
add_subdirectory(src/dnf5-plugin)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/libdnf-plugin-txnupd-0.3.0/doc/dnf5-txnupd.8.scd
new/libdnf-plugin-txnupd-0.4.0/doc/dnf5-txnupd.8.scd
--- old/libdnf-plugin-txnupd-0.3.0/doc/dnf5-txnupd.8.scd 2026-05-31
19:55:44.000000000 +0200
+++ new/libdnf-plugin-txnupd-0.4.0/doc/dnf5-txnupd.8.scd 2026-11-06
16:30:26.000000000 +0100
@@ -1,4 +1,4 @@
-dnf5-txnupd(8) "libdnf-plugin-txnupd 0.2.0" "System Administration"
+dnf5-txnupd(8) "libdnf-plugin-txnupd" "System Administration"
# NAME
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/libdnf-plugin-txnupd-0.3.0/src/dnf5-plugin/CMakeLists.txt
new/libdnf-plugin-txnupd-0.4.0/src/dnf5-plugin/CMakeLists.txt
--- old/libdnf-plugin-txnupd-0.3.0/src/dnf5-plugin/CMakeLists.txt
2026-05-31 19:55:44.000000000 +0200
+++ new/libdnf-plugin-txnupd-0.4.0/src/dnf5-plugin/CMakeLists.txt
2026-11-06 16:30:26.000000000 +0100
@@ -8,6 +8,7 @@
target_link_libraries(txnupd_dnf5_cmd PRIVATE
PkgConfig::LIBDNF5
PkgConfig::LIBDNF5_CLI
+ PkgConfig::TUKIT
${DNF5_LIB}
txnupd_util
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/libdnf-plugin-txnupd-0.3.0/src/libdnf5-plugin/txnupd.cpp
new/libdnf-plugin-txnupd-0.4.0/src/libdnf5-plugin/txnupd.cpp
--- old/libdnf-plugin-txnupd-0.3.0/src/libdnf5-plugin/txnupd.cpp
2026-05-31 19:55:44.000000000 +0200
+++ new/libdnf-plugin-txnupd-0.4.0/src/libdnf5-plugin/txnupd.cpp
2026-11-06 16:30:26.000000000 +0100
@@ -14,10 +14,13 @@
*/
// Standard library includes
+#include <chrono>
#include <cstdint>
#include <cstdlib>
+#include <ctime>
#include <filesystem>
#include <iostream>
+#include <stdexcept>
#include <string>
// POSIX standard library includes
@@ -64,12 +67,48 @@
const char * get_name() const noexcept override { return "TxnUpdError"; }
};
+/* Minimal logger for use from finish().
+ *
+ * libdnf5 calls the plugins' finish() hook while the Base is being destroyed,
+ * at which point Base::get_logger() is no longer usable (the log router is
+ * destroyed before the plugins), so dnf5.log can not be written any more.
+ *
+ * On an interactive terminal only warnings and errors are shown here, the
+ * informational messages are printed in plain form by the caller. Otherwise
+ * (cron, systemd units, scripts, ...) nobody sees that plain form, so emit
+ * everything down to INFO as timestamped log lines on stderr, where the
+ * caller's journal or log file picks them up.
+ */
+class FinishLogger : public libdnf5::Logger {
+public:
+ void write(
+ const std::chrono::time_point<std::chrono::system_clock> & time,
+ pid_t pid,
+ Level level,
+ const std::string & message) noexcept override {
+ const bool interactive = isatty(STDOUT_FILENO);
+ if (level > (interactive ? Level::WARNING : Level::INFO))
+ return;
+ if (interactive) {
+ std::cerr << message << std::endl;
+ return;
+ }
+ const std::time_t t = std::chrono::system_clock::to_time_t(time);
+ std::tm tm{};
+ char stamp[32] = "";
+ if (localtime_r(&t, &tm))
+ std::strftime(stamp, sizeof(stamp), "%Y-%m-%dT%H:%M:%S%z", &tm);
+ std::cerr << stamp << " [" << pid << "] " << level_to_cstr(level) << "
" << message << std::endl;
+ }
+};
+
class TxnUpd : public plugin::IPlugin2_1 {
private:
bool transactional_update = false;
bool ext_transactional_update = false;
bool apply_after_transaction = false;
bool print_reboot_msg = false;
+ bool finalize_pending = false;
TransactionalUpdate::Transaction * txnupd_transaction = nullptr;
std::string base_snapshot_id;
std::string snapshot_id;
@@ -147,9 +186,23 @@
* updates. The object is manually instantiated to avoid failures
* before the checks evaluate in the plugin.
*/
- base_snapshot_id =
TransactionalUpdate::SnapshotFactory::get()->getDefault();
- txnupd_transaction = new TransactionalUpdate::Transaction{};
- txnupd_transaction->init("default");
+ /* tukit reports failures (for example a failing snapper plugin) by
+ * throwing exceptions that are not libdnf5 errors. dnf5 only handles
+ * libdnf5::Error and std::runtime_error, so if they escape the
+ * process is terminated with a core dump. dnf5 does report a
+ * std::runtime_error cleanly, so translate them into one.
+ */
+ try {
+ base_snapshot_id =
TransactionalUpdate::SnapshotFactory::get()->getDefault();
+ txnupd_transaction = new TransactionalUpdate::Transaction{};
+ txnupd_transaction->init("default");
+ } catch (const std::exception & e) {
+ /* Deleting the transaction aborts a snapshot that was created but
+ * could not be set up. */
+ delete txnupd_transaction;
+ txnupd_transaction = nullptr;
+ throw std::runtime_error(std::string("txnupd plugin : Failed to
create a snapshot: ") + e.what());
+ }
/* Let's check to see if we succeeded in creating a snapshot before
* we try to proceed any further.
@@ -221,10 +274,53 @@
logger.warning("txnupd plugin : No package changes in transaction,
skipping snapshot finalization.");
return;
}
- /* Finally, complete the transaction and finalize
- * the update for next boot.
+ /* The snapshot must not be finalized here. libdnf5 still holds the
+ * rpm database of the snapshot (installroot) open at this point and
+ * only closes it - which checkpoints the sqlite WAL and so writes to
+ * the memory mapped -shm file - after all post_transaction hooks have
+ * run. Finalizing makes the snapshot read-only and unmounts it, so
+ * that last write would fail with SIGBUS. Defer it to finish(),
+ * which runs after the rpm transaction has been freed.
*/
- txnupd_transaction->finalize();
+ logger.debug("txnupd plugin : Deferring finalization of snapshot #" +
snapshot_id + " until libdnf5 is done.");
+ finalize_pending = true;
+
+ return;
+ }
+
+ /* Complete the transaction and finalize the update for next boot.
+ *
+ * Called by libdnf5 when the Base is destroyed, i.e. after the rpm
+ * transaction (and with it the rpm database in the snapshot) has been
+ * closed. Neither Base nor its logger may be used from here on.
+ */
+ void finish() noexcept override {
+ if (!finalize_pending || txnupd_transaction == nullptr)
+ return;
+ finalize_pending = false;
+
+ FinishLogger logger;
+ std::string failure;
+ try {
+ txnupd_transaction->finalize();
+ } catch (const std::exception & e) {
+ failure = e.what();
+ } catch (...) {
+ /* tukit plugins report failures by throwing their exit status */
+ failure = "unknown error";
+ }
+ if (!failure.empty()) {
+ /* The snapshot is not activated. Let tukit's destructor abort it
+ * and do not pretend that the update worked: finish() cannot
+ * report errors to dnf5, so end the process with a failure
+ * status instead of returning normally. */
+ delete txnupd_transaction;
+ txnupd_transaction = nullptr;
+ std::cout.flush();
+ logger.error("txnupd plugin : Could not finalize snapshot #" +
snapshot_id + ": " + failure);
+ logger.error("txnupd plugin : The changes will NOT be active after
a reboot!");
+ std::_Exit(EXIT_FAILURE);
+ }
delete txnupd_transaction;
txnupd_transaction = nullptr;
@@ -249,8 +345,6 @@
logger.info("txnupd plugin : Please reboot your machine to activate
the changes and avoid data loss.");
print_reboot_msg = true;
}
-
- return;
}
};
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/libdnf-plugin-txnupd-0.3.0/src/txnupd_util.cpp
new/libdnf-plugin-txnupd-0.4.0/src/txnupd_util.cpp
--- old/libdnf-plugin-txnupd-0.3.0/src/txnupd_util.cpp 2026-05-31
19:55:44.000000000 +0200
+++ new/libdnf-plugin-txnupd-0.4.0/src/txnupd_util.cpp 2026-11-06
16:30:26.000000000 +0100
@@ -33,6 +33,17 @@
namespace {
+// True if path exists, is a regular file (symlinks are followed) and has an
+// execute permission bit set.
+bool is_program(const std::filesystem::path & path) {
+ std::error_code ec;
+ const auto st = std::filesystem::status(path, ec);
+ if (ec || !std::filesystem::is_regular_file(st))
+ return false;
+ using std::filesystem::perms;
+ return (st.permissions() & (perms::owner_exec | perms::group_exec |
perms::others_exec)) != perms::none;
+}
+
std::string run_output(const std::string & cmd) {
std::array<char, 512> buf;
std::string result;
@@ -183,6 +194,27 @@
}
}
+ // Changes made to the running /etc after the snapshot was created exist
+ // only in the old /etc and would be hidden by the rbind below. Merge them
+ // into the snapshot's /etc first, as the boot-time sync (and
+ // `transactional-update apply`) does. Only possible if the snapshot has
+ // a sync point, i.e. /etc is a subvolume created by tukit's snapper
plugin.
+ const std::filesystem::path snapshot_etc =
std::filesystem::path("/.snapshots") / snapshot_id / "snapshot/etc";
+ const std::filesystem::path syncpoint = snapshot_etc / "etc.syncpoint";
+ const std::filesystem::path sync_etc_state =
"/usr/libexec/transactional-update-sync-etc-state";
+ if (is_program(sync_etc_state) && std::filesystem::exists(syncpoint)) {
+ logger.info("txnupd_util : Syncing /etc state from running system...");
+ if (isatty(STDOUT_FILENO))
+ std::cout << "Syncing /etc state from running system..." <<
std::endl;
+ int ret = run_argv({sync_etc_state.string(), "/etc",
snapshot_etc.string(), syncpoint.string()});
+ if (ret != 0) {
+ cleanup();
+ throw std::runtime_error(
+ "apply-live: syncing /etc state failed (exit code " +
std::to_string(ret) +
+ "), the running system was not modified");
+ }
+ }
+
logger.info("txnupd_util : Applying snapshot " + snapshot_id + " to
running system...");
if (isatty(STDOUT_FILENO))
std::cout << "Applying snapshot " << snapshot_id << " to running
system..." << std::endl;