Skip to content

Instantly share code, notes, and snippets.

@ruario
Last active December 22, 2023 18:14
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ruario/a36052a1ae1de4edbc6ad39fe39e5385 to your computer and use it in GitHub Desktop.
Save ruario/a36052a1ae1de4edbc6ad39fe39e5385 to your computer and use it in GitHub Desktop.
Uninstalling (removing) a package installed via make install. No make uninstall target required

make install, uninstall help (howto remove)

Background

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.

Using find

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")
find /etc /opt /usr -newerct @$(($c-${2:-10})) ! -newerct @$(($c+${2:-10})) ! -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/man/man1/zstdcat.1
/usr/local/man/man1/zstd.1
/usr/local/man/man1/unzstd.1
/usr/local/man/man1/zstdgrep.1
/usr/local/man/man1/zstdless.1
/usr/local/include/zbuff.h
/usr/local/include/zstd_errors.h
/usr/local/include/zstd.h
/usr/local/include/zdict.h
/usr/local/lib/libzstd.a
/usr/local/lib/libzstd.so.1
/usr/local/lib/libzstd.so.1.4.4
/usr/local/lib/libzstd.so
/usr/local/lib/pkgconfig/libzstd.pc
/usr/local/bin/zstdless
/usr/local/bin/unzstd
/usr/local/bin/zstdgrep
/usr/local/bin/zstdmt
/usr/local/bin/zstdcat
/usr/local/bin/zstd

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.


Tips and tricks

Backing up files before deleting them

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 removed_@1544621820.cpio, which I can later use to restore the files, like so:

cpio -imdv < removed_@1544621820.cpio

Alternate versions

For a very old distribution with a version of GNU find that precedes version 4.3 (e.g. RHEL 5) use the following:

#!/bin/sh -eu
c=$(stat -c%Z "$1")
s="$(mktemp -t find.XXXXXX)"
e="$(mktemp -t find.XXXXXX)"
touch -t $(date -d@$(($c-${2:-10})) +%Y%m%d%H%M.%S) "$s"
touch -t $(date -d@$(($c+${2:-10})) +%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 exceptionally slow version:

#!/bin/sh -eu
c=$(stat -c%Z "$1")
find /etc /opt /usr ! -type d -print0 | xargs -P4 -0I@ sh -c 't=$(stat -c%Z "@");[ $t -ge '$(($c-${2:-10}))' ]&&[ $t -le '$(($c+${2:-10}))' ]&&echo "@"'

Or for something quicker (but potentially less reliable), here is a version using mtime with a 3 minute resolution window around the reference file:

#!/bin/sh -eu
m=`expr \`expr \\\`date +%s\\\` - \\\`stat -c%Y "$1"\\\`\` / 60`
find /etc /opt /usr -mmin +`expr $m - ${2:-1} | sed 's/-.*/0/'` -mmin -`expr $m + 1 + ${2:-1}` ! -type d

Note: I used escaped, backtick command substitution in the above example, purely to get that ‘old school’ feel. 😉

Logging an install

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.

Uninstall using a pre-prepared install log

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.

Empty directories

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.

Removing them anyway

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment