# 'make install', uninstall help (howto remove manually installed software) 🐧 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. ℹ While technically directories are a type of file, they are intentionally ignored: § 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 ``` ℹ If your system does not support '-newerct', use an alternate version of this script: § 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. => https://en.wikipedia.org/wiki/Unix_time UNIX time => https://en.wikipedia.org/wiki/Stat_(system_call)#ctime Change time (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. ℹ '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 => gopher://bombadillo.colorfield.space Bombadillo (non-web) browser 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 ``` ℹ 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'. => https://www.gnu.org/prep/standards/html_node/DESTDIR.html 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='. 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 / ```` ℹ 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 ``` ℹ 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 [§ Logging an install ¶ 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. ℹ For more background, the Filesystem Hierarchy Standard (3.0) documents the standard directories you can expect to find on a Linux system. => http://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html FHS 3.0 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' ℹ 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' ``` ℹ 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. => https://gist.github.com/ruario/a36052a1ae1de4edbc6ad39fe39e5385 Original 'Gist' of the above text (Github) ⁂ => ../contact.gmi 📝 Comment => . 🔙 Gemlog index => .. 🔝 Capsule index