Perform Manual dnf history undo
My choices of GNU/Linux distributions are Fedora for desktop computers and
CentOS for servers. Both of them feature DNF - Red Hat’s package manager
front-end for RPM, which is the primary reason I use these distributions.
Although one might argue that it is slow, DNF is very clear on not only what it
will do for an action but also what it has done: it maintains a history of
package installation and removal operations, which are called “transactions”.
You can undo a transaction with dnf history undo
or roll back to a previous
state with dnf history rollback
. Other common package managers, like APT and
Pacman, also save operation logs, but they generally do not offer a similar
operation reverting functionality.
dnf history undo
has been helping me keep my system clean. If I have
installed something with DNF but find that it is not what I want, I can
uninstall it, along with every dependency, simply with dnf history undo last
.
When dnf history undo
Fails
dnf history undo
has been working consistently fine if I run it for a
transaction performed recently, but for transactions ran a while ago, it might
fail.
A month ago, I installed Wine on my laptop so I could play some retro Windows games on Fedora. After a few weeks, an incompatibility issue of Wine made me go back to Windows for gaming, so I decided to uninstall Wine.
The first command I ran was dnf history list wine
, which would give me a list
of all transactions that involved the wine
package. The output showed that
transaction 217 was the one in which wine
had been installed.
Alright, so all I would need to run was dnf history undo 217
. However, it did
not work as expected…
I had not removed those Wine packages yet, but why did DNF tell me that no
package was matched? It was because transaction 217 (log) installed Wine 5.10, but transaction 229 (log), which updated my system, upgraded
Wine to 5.12. dnf history undo
identifies packages by combinations of package
name, version and architecture instead of merely the names, so it does not
treat wine-5.10-1.fc32.x86_64
and wine-5.12-1.fc32.x86_64
as equivalent
packages.
Therefore, if you have upgraded any package involved in a transaction, then
dnf history undo
will fail for that transaction. This is why that command
has been doing its job when I run it for a recent transaction but would emit an
error for a transaction performed a while ago.
What About dnf remove
?
The dnf remove
command is capable of removing unused dependencies along with
the packages requested to be removed, so theoretically, dnf remove wine
would
do the same thing as dnf history undo 217
if the packages were not upgraded.
But, when I ran it…
Only 83 packages would be removed by the command. The output of dnf history list wine
suggested that 197 packages were installed in transaction 217. If I
had chosen to proceed, 114 unused dependencies would have been left in my
system.
Manually Undo the Transaction
Although undoing transaction 217 with dnf history undo
was not possible, the
dnf history info 217
command could still give the list of packages installed
in that transaction in its output. I
could gather those packages’ names and supply them as arguments to dnf remove
in order to perform a complete uninstallation.
Here are some lines in the command output:
...
Install pulseaudio-libs-13.99.1-4.fc32.i686 @updates
Install samba-common-tools-2:4.12.3-0.fc32.1.x86_64 @updates
Install samba-libs-2:4.12.3-0.fc32.1.x86_64 @updates
Install samba-winbind-2:4.12.3-0.fc32.1.x86_64 @updates
Install samba-winbind-clients-2:4.12.3-0.fc32.1.x86_64 @updates
Install samba-winbind-modules-2:4.12.3-0.fc32.1.x86_64 @updates
Install sane-backends-drivers-cameras-1.0.30-1.fc32.i686 @updates
Install sane-backends-drivers-scanners-1.0.30-1.fc32.i686 @updates
Install sane-backends-libs-1.0.30-1.fc32.i686 @updates
Install spirv-tools-libs-2019.5-2.20200421.git67f4838.fc32.i686 @updates
Install spirv-tools-libs-2019.5-2.20200421.git67f4838.fc32.x86_64 @updates
...
The packages are listed in their NEVRAs - Name, Epoch, Version, Release,
and Architecture - in the form of name-[epoch:]version-release.arch
. When a
package is upgraded, its [epoch:]version-release
changes, but name
and
arch
will always be constant. Thus, I should convert those NEVRAs from
name-[epoch:]version-release.arch
to only name.arch
.
There were 197 packages, and processing all of them by hand would be tedious and error-prone, so I looked for the way to do the conversion with Unix programs.
First and foremost, I used grep
to reduce the command output so it would only
contain lines with a NEVRA. At the beginning of the output were some lines
containing the transaction’s metadata, which should be removed. I also saved
the reduced output to a file for subsequent processing.
$ sudo dnf history info 217 | grep 'Install' > /tmp/installed-pkgs.txt
The next thing that should be done was to trim each line so it only contains
the NEVRA itself. This would require me to remove the word “Install”, the
repository ID starting with @
, and any white space. Replacing all occurrences
of a string with another string sounds like a job for sed
. I used -i
flag
for in-place modification and -E
for extended regular expression support.
$ sed -i -E 's/^ *Install *//g' /tmp/installed-pkgs.txt
$ sed -i -E 's/ *@.+$//g' /tmp/installed-pkgs.txt
At this point, every line in the file contained only the NEVRA in the form of
name-[epoch:]version-release.arch
. The next step would be further reducing
those lines to the form of name.arch
. To do this, I needed to come up with a
regular expression for -[epoch:]version-release
. The following one is the
best I could make:
-([0-9]+:)?[0-9][0-9A-Za-z.-]*\.(el|fc)[0-9]+(_[0-9]+)?(\.[0-9]+)?
1~~^^~~~~2~~~^^~~~~~~~~3~~~~~~~~^^~~~~~~~~~~~4~~~~~~~~~~~^^~~~~5~~~~^
As annotated, this regular expression consists of five parts:
-
The dash
-
is the separator between package name and versioning information. -
This part is for
epoch
. The purpose of epoch is described here in Fedora documentation. Not all packages have it, so this part ends with a question mark?
, meaning that it matches 0 or 1 of the preceding group in the parentheses. -
This section matches
version
and part ofrelease
. The version should start with a digit and may contain multiple digits and dots.
, but chances are it also contains letters, as intzdata-2020a
. Letters may also exist in release, likecrontabs-1.11-22.20190603git
for example. -
This is the value of
%{?dist}
tag in RPM macros, which marks the distribution release this package was built for. Example values include.fc32
,.el8
, and.el8_2
. -
Some packages might have extra minor release bump after
%{?dist}
. Like the part for epoch, it also ends with a question mark.
This regular expression should be good for most packages on Fedora and packages
on CentOS and RHEL that stick with Fedora’s package versioning guidelines. I
only know one exception, ntfs-3g-system-compression-1.0-3.fc32.x86_64
,
because it has a digit right after a dash in its package name. Unless you are
dealing with a package whose name meets this criterion, which should be rare,
this issue does not matter.
As a double check, you can first try to see how this regular expression matches
the -[epoch:]version-release
part of every NEVRA with the following command:
$ grep -E -- '-([0-9]+:)?[0-9][0-9A-Za-z.-]*\.(el|fc)[0-9]+(_[0-9]+)?(\.[0-9]+)?' /tmp/installed-pkgs.txt
The matched parts are shown in red. If everything looks good, you can make the
modification with sed
.
$ sed -i -E 's/-([0-9]+:)?[0-9][0-9A-Za-z.-]*\.(el|fc)[0-9]+(_[0-9]+)?(\.[0-9]+)?//g' /tmp/installed-pkgs.txt
I finally got the list of packages specified in the form of name.arch
in a
file. All I needed to do at this moment was to pass in every line of the file
to the dnf remove
command as argument. This is where xargs
came in handy.
The -a
option was for telling xargs
to read the arguments from the
specified file instead of standard input.
$ xargs -a /tmp/installed-pkgs.txt sudo dnf remove
The transaction summary showed that 197 packages would be removed. No unused packages would remain in my system, and no unrelated packages were accidentally erased. When you are doing this step, please double check the number of packages which will be altered as well to prevent unexpected changes to your system. Should you find a mismatch, review any error messages from DNF and your package list file.
The manual reversal of the transaction with the help of dnf history info
,
grep
, sed
, xargs
and dnf remove
was successful. This is yet another
example for the Unix philosophy: multiple general-purpose tools were used
together to complete a specialized and complex task, and they interfaced with
each other through nothing but text streams.
A Single Command for the Whole Task
In the steps above, I used a file /tmp/installed-pkgs.txt
to observe how
every command changed the package list more conveniently. Creation of the file
is not necessary: once you are confident about what each command does so you do
not need to study how the list is changed, you can use pipes to let each
command pass its standard output directly to the next command as the standard
input, so no file creation is required at all. This also transforms the whole
process to a single long command.
$ sudo dnf history info <transaction-id> | \
grep 'Install' | \
sed -E 's/^ *Install *//g' | \
sed -E 's/ *@.+//g' | \
sed -E 's/-([0-9]+:)?[0-9][0-9A-Za-z.-]*\.(el|fc)[0-9]+(_[0-9]+)?(\.[0-9]+)?//g' | \
xargs -o sudo dnf remove
Some notable changes to the commands include:
-
The
-i
flag forsed
is no longer used. Without this flag,sed
will print the edited text to the standard output. Because the standard output will be passed to the next program’s standard input by a pipe, this is the desired behavior. -
Since there is no file involved, the
-a
option forxargs
is no longer needed. Instead, the-o
flag is used here so you can make input from your keyboard to the program you run withxargs
. DNF will ask for your confirmation before it performs the requested operation, and the-o
option ofxargs
will allow you to answery
to DNF.