A common mistake for users who are new to Linux (and even a few seasoned users) is to install a package from source without any clear idea about how they will remove it in the future, should they want to.
The classic instructions to install a source package are ./configure && make && make install
. This (or slight variants) can work nicely for installation but
instructions for clean removal of the package are typically absent. While some
source packages include a make uninstall
target, there are no guarentees that
it works correctly. Software developers will go to great lengths to test
installation but they generally care far less about uninstall, as they never
imagine a user wanting to remove their wonderful software. Worse, removal
commands can be pretty high risk if they are buggy.
You can use find
to locate all files (excluding
directories) associated with a package, if you know just
one file provided by the package. The following is a shell script that will
automate finding files that are likely to be related to your reference file,
based on common install time. Just run it as root (or prefaced with sudo
)
providing a single argument, that being the path to the chosen reference file.
#!/bin/sh -eu
c=$(stat -c%Z "$1")
r=${2:-10}
find /etc /opt /usr -newerct @$(($c-$r)) ! -newerct @$(($c+$r)) ! -type d
Note: In the unlikely event that the version of find
on your system does
not support -newerct
, use an alternate version of this
script.
This works by noting the ‘UNIX timestamp’ for ‘Change (ctime)’ on the reference file. It then takes 10 seconds either side of this time as a range to locate files that were installed (or ‘changed’) at approximately the same time.
Note: ‘Modify (mtime)’ would be far less reliable, as the original modification time is often retained on files during installation.
I have Zstandard compiled and installed from
source. Running the above script with /usr/local/bin/zstd
as the only argument
gives me the following result:
/usr/local/include/zdict.h
/usr/local/include/zbuff.h
/usr/local/include/zstd.h
/usr/local/include/zstd_errors.h
/usr/local/lib/libzstd.so.1.3.7
/usr/local/lib/libzstd.a
/usr/local/lib/libzstd.so
/usr/local/lib/libzstd.so.1
/usr/local/lib/pkgconfig/libzstd.pc
/usr/local/share/man/man1/zstdcat.1
/usr/local/share/man/man1/unzstd.1
/usr/local/share/man/man1/zstdless.1
/usr/local/share/man/man1/zstdgrep.1
/usr/local/share/man/man1/zstd.1
/usr/local/bin/unzstd
/usr/local/bin/zstd
/usr/local/bin/zstdcat
/usr/local/bin/zstdmt
/usr/local/bin/zstdless
/usr/local/bin/zstdgrep
Note: If a second argument is provided, the script will interpret it as seconds before and after the reference file's Change time (otherwise 10 seconds is assumed). Increase the value if you think some files may have been missed. Decrease it if you feel that too many files were found.
To delete the files, pass the results though a pipe to xargs -d\\n rm -v
(as
root or by adding sudo
before xargs
).
Note: Make sure you are 100% satisfied before commiting to deletion (or read this).
This is the end of the short guide but read on if you want some “tips and tricks” on better ways handle these kinds of situations in the future.
Before removing anything, you might want to make a backup of the matched files.
You can do this by piping the result to an archiver like cpio
or tar
(with
appropriate options). For example | cpio -ovHnewc > removed_@$(date +%s).cpio
.
On my system, this created the archive [email protected]
, which I can
later use to restore the files, like so:
cpio -imdv < [email protected]
For a very old distribution with a version of GNU find
that precedes version
4.3.3 (e.g. RHEL 5) use the following:
#!/bin/sh -eu
c=$(stat -c%Z "$1")
r=${2:-10}
s="$(mktemp -t find.XXXXXX)"
e="$(mktemp -t find.XXXXXX)"
touch -t $(date -d@$(($c-$r)) +%Y%m%d%H%M.%S) "$s"
touch -t $(date -d@$(($c+$r)) +%Y%m%d%H%M.%S) "$e"
find /etc /opt /usr -cnewer "$s" ! -cnewer "$e" ! -type d ||:
rm "$s" "$e"
For a distribution that does not use GNU find
and understands neither
-newerct
nor -cnewer
(e.g. busybox-based), you could try this very
slow version:
#!/bin/sh -eu
c=$(stat -c%Z "$1")
r=${2:-10}
find /etc /opt /usr ! -type d -print0 | xargs -0P12 -I@ \
sh -c 't=$(stat -c%Z "@");if [ $t -ge '$(($c-$r))' -a $t -le '$(($c+$r))' ];then echo "@";fi'
Rather than attempting to find files associated with a package some time in the
future, you should instead make the log immediately after you first installed
the software. This is safer because you will have a valid log even if a Change
timestamp on some file(s) gets altered in the future (intentionally or by
acident). Just run the script right after make install
completes and redirect
the output to a file.
An even better way to make a log is to do it before you install. That way you can be certain that the log only contains files that you have placed onto the system. You will need a little knowledge of Linux packaging to pull this one off—if you have a lot of packaging knowledge, step up and make a real package, since that is an even better idea.
Most software on Linux can have its install step redirected to “staging”
directory, rather than straight onto the root filesystem. A common way to do
this is via DESTDIR. Rather than the typical ./configure && make && make install
combo, the following would be done:
./configure
make
make install DESTDIR=/path/to/staging
If we set DESTDIR to "$PWD/staging", then after installation is complete, we can do the following to create our log
cd staging
find * \! -type d -printf '/%p\n' | tee ../program-name_files.log
You now have a log that can only contain the files that form part of the
package. After the command completes, step back up a directory and re-issue
make install
, without DESTDIR="$PWD/staging"
.
An alternative install option would be to copy the files from the staging
directory to the root (/) directory yourself. i.e. from within “$PWD/staging”
you could issue the following (place sudo
in front of cpio
if you are not
already root):
find . \! -type d -print0 | cpio -p0mdv /
Note: For permissions to be correct, the above assumes you did your earlier
make install "$PWD/staging"
as root (or with sudo
). If not, either correct
the permissions before installing them with a recursive chown
or you could add
something like -R 0:0
to the cpio command to reset everything to "root:root"
during the copy.
To delete files listed in a log, just issue the following as root (or prefaced with sudo).
xargs -d\\n rm -v < program-name_files.log
Note: The logs created by the above methods are pretty safe but you could
have problems if the package includes files with very unusual names. For
example, theoretically *nix files can have new lines (line feeds) in their names
and those would not be handled well. If you want to avoid this (exceptionally
unlikely) scenario, use the before install method but create the log with
'/%p\0'
instead to make it null-byte seperated. On uninstall replace xargs -d\\n
with xargs -0
.
All typical filetypes (including symlinks) can be removed by the above methods but not directories. They were intentionally omitted from output, since they may have been system directories, shared with other software present on your system. Therefore you need to be a little bit more cautious. For the most part empty directories cause no problems and generally have negligible space requirements. So you can—and probably should—just ignore them.
However, if you are the type of person who can't let that go, you can construct
a find
command to track down old empty directories that you might want to
consider removing. The parent directories that are non-shared are usually really
easy for a human to spot. Unlike the package files which can have a variety of
names, non-shared directories (at least the parent ones) will generally be named
after the package in some way, with the occasional variation in casing and/or
the addition of the version number.
There is no official standard for this but it happens almost universally for fairly obvious reasons. Directories are used to separate a program's commonly named files from the rest of the system, and so the directory itself needs a unique name. Given all applications try to have unique package names anyway (to avoid confusion with other packages), the obvious solution for the package maintainer is to use the package name for any non-shared (a.k.a. non-standard) directories.
Note: For more background, the “Filesystem Hierarchy Standard (3.0)” documents the standard directories you can expect to find on a Linux system.
Imagine an hypothetical application installed from a source package called “example-program-1.0.tar.gz”. After removing all files associated with it, you might choose to run the following command to look for empty directories:
find /etc /opt /usr -type d -empty
Amongst the results, you might then notice the following:
/usr/local/share/ExampleProgram_1.0/level1subdirectory1
/usr/local/share/ExampleProgram_1.0/level1subdirectory2
/usr/local/share/ExampleProgram_1.0/level1subdirectory3/level2subdirectory1
/usr/local/share/ExampleProgram_1.0/level1subdirectory3/level2subdirectory2
/usr/local/share/ExampleProgram_1.0/level1subdirectory4
The parent, non-standard directory is therefore
“/usr/local/share/ExampleProgram_1.0
”
Note: You may not even need to run this extra find
, as you could have
spotted this directory in the output of the inital command used to locate all
associated files.
Once you have removed all files that belong to a package, it is trivial to safely remove empty directory trees, for example:
$ sudo find /usr/local/share/ExampleProgram_1.0 -depth -exec rmdir -v {} \;
rmdir: removing directory, '/usr/local/share/ExampleProgram_1.0/level1subdirectory1'
rmdir: removing directory, '/usr/local/share/ExampleProgram_1.0/level1subdirectory2'
rmdir: removing directory, '/usr/local/share/ExampleProgram_1.0/level1subdirectory3/level2subdirectory1'
rmdir: removing directory, '/usr/local/share/ExampleProgram_1.0/level1subdirectory3/level2subdirectory2'
rmdir: removing directory, '/usr/local/share/ExampleProgram_1.0/level1subdirectory3'
rmdir: removing directory, '/usr/local/share/ExampleProgram_1.0/level1subdirectory4'
rmdir: removing directory, '/usr/local/share/ExampleProgram_1.0'
Note: This above command is safe because rmdir
will only remove empty
directories.