'make install', uninstall help
==============================

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 guarantees 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 the find command to locate all files (excluding
directories) associated with a package, if you know just one file
provided by the package.

[i] While technically directories are a type of file, they are
intentionally ignored: S. Empty directories.

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

[i] If your system does not support '-newerct', use an alternate
version of this script: S. Tips and tricks - Alternate versions.

The script works by noting the ctime (the time of last inode metadata
status 'change') since the UNIX Epoch for the reference file. It then
takes 10 seconds either side of this and uses that as a range to
locate files that were changed (installed), in the most common
installation directories, during approximately the same period.

~ UNIX time:
https://en.wikipedia.org/wiki/Unix_time

~ Change time (ctime):
https://en.wikipedia.org/wiki/Stat_(system_call)#ctime

If a second argument is provided, the script will interpret it as
seconds before and after the reference file's ctime (instead of the
default 10). You can increase the value if you think some files may
have been missed or decrease it if you feel that too many files were
found.

[i] 'mtime' (when the file's contents--rather than inode
metadata--were 'modified') would be less reliable, as the original
mtime of files within the source archive is occasionally retained
during installation. With ctime that cannot happen, since the inode
metadata must be updated to write the file to the new location on
disk. The 'birth time' (when the file was first 'created') would
theoretically be even better because by definition, birth time never
changes. However, few filesystems support this, so it cannot be used
in the overwhelming majority of cases.

-Example output-

~ Bombadillo (non-web) browser:
gopher://bombadillo.colorfield.space

I have Bombadillo compiled and installed from source. Running the
above script (named 'siblings.sh' for this example) with
'/usr/local/bin/bombadillo' as the only argument, produces the
following result.

# ./siblings.sh /usr/local/bin/bombadillo
/usr/local/share/pixmaps/bombadillo-icon.png
/usr/local/share/man/man1/bombadillo.1.gz
/usr/local/share/applications/bombadillo.desktop
/usr/local/bin/bombadillo

To delete the files, pass the results though a pipe to 'xargs -d\\n 
rm -v' (as root or by adding sudo before xargs). You should make 
sure you are 100% satisfied that nothing extra or unexpected is 
listed before committing to deletion. If there is, filter that out 
first.

-Are we done yet?-

Above this point is the short version of this guide. If you got what
you came for and removed an unwanted package, you may well be done.
However, if you had issues with the script or want to understand more,
feel free to read on...

* * *

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).

./siblings.sh /usr/local/bin/bombadillo | cpio -ovHnewc > bombadillo_@$(date +%s).cpio

On my system, this created the archive 'bombadillo_@1637674620.cpio',
which I can later use to restore the files, like so.

cpio -imdv < bombadillo_@1637674620.cpio

-Alternate versions-

For a distribution that does not use GNU find and does not understand
'-newerct' (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 "@"'

Here is another version, this time using mtime with a 3 minute
resolution window around the reference file, thus making it much less
accurate but massively faster!

Oh... and I decided to use escaped, backtick command substitution
here, purely to get that 'old school' feel (though it could
potentially also mean that it works in older environments). I shall
leave it as an exercise to you (the reader), to convert it to a more
modern style version, if you hate this arbitrary decision on my part.
:*

#!/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

[i] The macOS 'stat' command (and likely *BSD-based systems more
generally) does not understand '-c%Y'. You can tweak the above example
to use '-f%m' on such systems.

-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 ctime on some file(s) gets altered in the future
(intentionally or by accident). 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'.

~ DESTDIR:
https://www.gnu.org/prep/standards/html_node/DESTDIR.html

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='.

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 /

[i] 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

[i] 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 [S. Logging an install P. 2] but create the log with
'/%p\0' instead to make it null-byte separated. On uninstall replace
'xargs -d\\n' with 'xargs -0'.

-Empty directories-

All typical file types (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 already present (or that might be installed in the
future). 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.

If you are the type of person who can't handle having redundant
directories, 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.

[i] For more background, the Filesystem Hierarchy Standard (3.0)
documents the standard directories you can expect to find on a Linux
system.

~ FHS 3.0:
http://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html

Imagine a hypothetical application installed from a source package
called 'example-program-1.0.tar.gz'. After removing all installed
files related to it using the method outlined at the start of this
guide, you could then 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

Here the parent, non-standard directory is obviously
'/usr/local/share/ExampleProgram_1.0'

[i] You may not even need to run this extra find, as you could have
spotted this directory in the output of the initial command used to
locate all related files.

It is trivial to safely remove non-standard, empty directory trees,
using the noted path(s) via another find command.

# 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'

[i] This above command is safe because rmdir will only remove empty
directories.

And that, my dear reader, is it. I hope it helped! ;)

* * *

This posting was adapted from a Github Gist that I wrote. It never
really felt like a 'gist' though, so perhaps it makes more sense here.

~ Original 'Gist' of the above text (Github):
https://gist.github.com/ruario/a36052a1ae1de4edbc6ad39fe39e5385

* * *