From 4e5d104e48a450bcbb795d288b9a67d7c7917daa Mon Sep 17 00:00:00 2001 From: hanez Date: Thu, 2 Oct 2025 23:01:22 +0200 Subject: [PATCH] Added uninstall routine to CMake. (0.17.4) --- CMakeLists.txt | 148 ++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 2 +- 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 59394fb..ba9fa29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fun VERSION 0.17.3 LANGUAGES C) +project(fun VERSION 0.17.4 LANGUAGES C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -188,6 +188,152 @@ add_custom_target(distclean COMMENT "Remove build outputs and CMake-generated files (reconfigure needed)" ) +# Robust uninstall support as a function (uses install_manifest*.txt) +function(fun_add_uninstall_target) + set(_FUN_UNINSTALL_SCRIPT "${CMAKE_BINARY_DIR}/cmake_uninstall.cmake") + + # Generate the cmake_uninstall.cmake script at configure time + file(WRITE "${_FUN_UNINSTALL_SCRIPT}" +"set(_candidates \"\") +# Prefer explicit MANIFEST if provided +if(DEFINED MANIFEST AND NOT MANIFEST STREQUAL \"\") + list(APPEND _candidates \"\${MANIFEST}\") +endif() + +# Common locations (out-of-source and legacy in-source builds) +list(APPEND _candidates + \"\${CMAKE_BINARY_DIR}/install_manifest.txt\" + \"\${CMAKE_SOURCE_DIR}/install_manifest.txt\" + \"\${CMAKE_BINARY_DIR}/install_manifest_Debug.txt\" + \"\${CMAKE_BINARY_DIR}/install_manifest_Release.txt\" + \"\${CMAKE_BINARY_DIR}/install_manifest_RelWithDebInfo.txt\" + \"\${CMAKE_BINARY_DIR}/install_manifest_MinSizeRel.txt\" + \"\${CMAKE_SOURCE_DIR}/install_manifest_Debug.txt\" + \"\${CMAKE_SOURCE_DIR}/install_manifest_Release.txt\" + \"\${CMAKE_SOURCE_DIR}/install_manifest_RelWithDebInfo.txt\" + \"\${CMAKE_SOURCE_DIR}/install_manifest_MinSizeRel.txt\" +) + +set(_manifest_file \"\") +foreach(_cand IN LISTS _candidates) + if(EXISTS \"\${_cand}\") + set(_manifest_file \"\${_cand}\") + break() + endif() +endforeach() + +if(NOT _manifest_file) + message(FATAL_ERROR + \"Cannot find install manifest. Tried: \${_candidates}. +If you installed from a different build directory, run uninstall from that build directory (where install_manifest*.txt resides).\") +endif() + +file(READ \"\${_manifest_file}\" _manifest) +string(REPLACE \"\\n\" \";\" _files \"\${_manifest}\") +set(_removed 0) +set(_dirs \"\") +foreach(_file IN LISTS _files) + if(_file STREQUAL \"\") + continue() + endif() + if(EXISTS \"\${_file}\" OR IS_SYMLINK \"\${_file}\") + message(STATUS \"Uninstalling: \${_file}\") + file(REMOVE \"\${_file}\") + # If the file still exists after attempting to remove it, fail with an explicit error + if(EXISTS \"\${_file}\" OR IS_SYMLINK \"\${_file}\") + message(FATAL_ERROR \"Failed to remove: \${_file}. Try running the uninstall target with sudo if it is a system install.\") + endif() + # Track the containing directory for potential cleanup + get_filename_component(_dir \"\${_file}\" DIRECTORY) + list(APPEND _dirs \"\${_dir}\") + math(EXPR _removed \"\${_removed}+1\") + else() + message(STATUS \"Skipping (not found): \${_file}\") + endif() +endforeach() + +# Remove empty directories that contained installed files (and their parents under safe roots) +# Safe roots we are allowed to prune if they become empty: +set(_safe_roots \"/usr/share/fun\" \"/usr/share/doc/fun\") + +# Expand to include parent directories up to the safe roots +set(_all_dirs \"\") +foreach(_d IN LISTS _dirs) + set(_p \"\${_d}\") + while(NOT \"\${_p}\" STREQUAL \"\" AND NOT \"\${_p}\" STREQUAL \"/\") + list(APPEND _all_dirs \"\${_p}\") + get_filename_component(_p \"\${_p}\" DIRECTORY) + # Stop collecting parents once we reach a safe root or the filesystem root + list(FIND _safe_roots \"\${_p}\" _root_idx) + if(_root_idx GREATER -1) + list(APPEND _all_dirs \"\${_p}\") + break() + endif() + endwhile() +endforeach() + +# Deduplicate +list(REMOVE_DUPLICATES _all_dirs) + +# Try multiple passes to remove parents that become empty after children are removed +set(_removed_dirs 0) +set(_pass 0) +while(TRUE) + set(_pass_removed 0) + foreach(_dir IN LISTS _all_dirs) + if(EXISTS \"\${_dir}\" AND IS_DIRECTORY \"\${_dir}\") + # Only act on directories within safe roots + set(_allowed FALSE) + foreach(_root IN LISTS _safe_roots) + if(\"\${_dir}\" MATCHES \"^\${_root}(/|\$)\") + set(_allowed TRUE) + break() + endif() + endforeach() + if(NOT _allowed) + # e.g. /usr/bin — do not remove + continue() + endif() + + # Remove only if empty (do not recurse unrelated content) + file(GLOB _dir_contents LIST_DIRECTORIES true \"\${_dir}/*\") + list(LENGTH _dir_contents _dir_len) + if(_dir_len EQUAL 0) + message(STATUS \"Removing empty directory: \${_dir}\") + file(REMOVE_RECURSE \"\${_dir}\") + if(IS_DIRECTORY \"\${_dir}\") + message(FATAL_ERROR \"Failed to remove directory: \${_dir}. Try running the uninstall target with sudo if it is a system install.\") + endif() + math(EXPR _removed_dirs \"\${_removed_dirs}+1\") + math(EXPR _pass_removed \"\${_pass_removed}+1\") + endif() + endif() + endforeach() + + math(EXPR _pass \"\${_pass}+1\") + if(_pass_removed EQUAL 0 OR _pass GREATER 5) + break() + endif() +endwhile() + +message(STATUS \"Uninstall finished. Removed \${_removed} files and \${_removed_dirs} directories (if empty) listed in \${_manifest_file}\") +") + + # Expose an 'uninstall' build target (run with sudo if system locations were used) + add_custom_target(uninstall + COMMAND ${CMAKE_COMMAND} + -D MANIFEST="${CMAKE_BINARY_DIR}/install_manifest.txt" + -D CMAKE_BINARY_DIR="${CMAKE_BINARY_DIR}" + -D CMAKE_SOURCE_DIR="${CMAKE_SOURCE_DIR}" + -P "${_FUN_UNINSTALL_SCRIPT}" + USES_TERMINAL + COMMENT "Uninstall files installed by this project (use sudo if needed)" + ) +endfunction() + +# Define the uninstall target +fun_add_uninstall_target() + # Install rules # Binary install(TARGETS fun diff --git a/README.md b/README.md index 6bc34aa..fbd2fe4 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ This requires Cygwin to be installed and configured. I will not cover this here. - [GNU](https://gnu.org/)/[Linux](https://kernel.org/) ([Arch](https://archlinux.org/)/[Artix](https://artixlinux.org/), [Debian](https://www.debian.org/)) using [GCC](https://gcc.gnu.org/) and the [GNU C library](https://www.gnu.org/software/libc/) ([glibc](https://en.wikipedia.org/wiki/Glibc)) - GNU/Linux ([Alpine](https://alpinelinux.org/)) using GCC and the [musl libc](https://musl.libc.org/) - [FreeBSD](https://www.freebsd.org/) using [Clang](https://clang.llvm.org/) and the [BSD libc](https://en.wikipedia.org/wiki/C_standard_library#BSD_libc) -- [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows){:class="ext"} using [Cygwin](https://www.cygwin.com/){:class="ext"} and GCC. +- [Windows](https://en.wikipedia.org/wiki/Microsoft_Windows) using [Cygwin](https://www.cygwin.com/) and GCC. ### Other systems