diff --git a/Help/command/install.rst b/Help/command/install.rst index 781fbbab84..42765ad94b 100644 --- a/Help/command/install.rst +++ b/Help/command/install.rst @@ -648,6 +648,7 @@ Signatures [USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER] [CONFIGURATIONS ...] [COMPONENT ] [EXCLUDE_FROM_ALL] + [EXCLUDE_EMPTY_DIRECTORIES] [FILES_MATCHING] [ ...]... ) @@ -750,6 +751,13 @@ Signatures Disable file installation status output. + ``EXCLUDE_EMPTY_DIRECTORIES`` + .. versionadded:: 4.1 + + Exclude empty directories from installation. A directory is + considered empty if it contains no files, no symbolic links, + and no non-empty subdirectories. + ``FILES_MATCHING`` This option may be given before the first ```` to disable installation of files (but not directories) not matched diff --git a/Help/release/dev/install-DIRECTORY-exclude-empty.rst b/Help/release/dev/install-DIRECTORY-exclude-empty.rst new file mode 100644 index 0000000000..fcdfe2314d --- /dev/null +++ b/Help/release/dev/install-DIRECTORY-exclude-empty.rst @@ -0,0 +1,6 @@ +install-DIRECTORY-exclude-empty +------------------------------- + +* The :command:`install(DIRECTORY)` command gained a new + ``EXCLUDE_EMPTY_DIRECTORIES`` option to skip installation + of empty directories. diff --git a/Source/cmFileCopier.cxx b/Source/cmFileCopier.cxx index c541bb9ac4..b2ce6b9e7b 100644 --- a/Source/cmFileCopier.cxx +++ b/Source/cmFileCopier.cxx @@ -25,6 +25,10 @@ #include #include +#include + +#include +#include using namespace cmFSPermissions; @@ -294,6 +298,13 @@ bool cmFileCopier::CheckKeyword(std::string const& arg) this->Doing = DoingNone; this->MatchlessFiles = false; } + } else if (arg == "EXCLUDE_EMPTY_DIRECTORIES") { + if (this->CurrentMatchRule) { + this->NotAfterMatch(arg); + } else { + this->Doing = DoingNone; + this->ExcludeEmptyDirectories = true; + } } else { return false; } @@ -647,6 +658,29 @@ bool cmFileCopier::InstallFile(std::string const& fromFile, return this->SetPermissions(toFile, permissions); } +static bool IsEmptyDirectory(std::string const& path, + std::unordered_map& cache) +{ + auto i = cache.find(path); + if (i == cache.end()) { + bool isEmpty = (!cmSystemTools::FileIsSymlink(path) && + cmSystemTools::FileIsDirectory(path)); + if (isEmpty) { + cmsys::Directory d; + d.Load(path); + unsigned long numFiles = d.GetNumberOfFiles(); + for (unsigned long fi = 0; isEmpty && fi < numFiles; ++fi) { + std::string const& name = d.GetFileName(fi); + if (name != "."_s && name != ".."_s) { + isEmpty = IsEmptyDirectory(d.GetFilePath(fi), cache); + } + } + } + i = cache.emplace(path, isEmpty).first; + } + return i->second; +} + bool cmFileCopier::InstallDirectory(std::string const& source, std::string const& destination, MatchProperties match_properties) @@ -719,6 +753,11 @@ bool cmFileCopier::InstallDirectory(std::string const& source, strcmp(dir.GetFile(fileNum), "..") == 0)) { std::string fromPath = cmStrCat(source, '/', dir.GetFile(fileNum)); std::string toPath = cmStrCat(destination, '/', dir.GetFile(fileNum)); + if (this->ExcludeEmptyDirectories && + IsEmptyDirectory(fromPath, this->DirEmptyCache)) { + continue; + } + if (!this->Install(fromPath, toPath)) { return false; } diff --git a/Source/cmFileCopier.h b/Source/cmFileCopier.h index 673ae03300..840e256ec9 100644 --- a/Source/cmFileCopier.h +++ b/Source/cmFileCopier.h @@ -5,6 +5,7 @@ #include "cmConfigure.h" // IWYU pragma: keep #include +#include #include #include "cmsys/RegularExpression.hxx" @@ -30,6 +31,7 @@ protected: char const* Name; bool Always = false; cmFileTimeCache FileTimes; + std::unordered_map DirEmptyCache; // Whether to install a file not matching any expression. bool MatchlessFiles = true; @@ -89,6 +91,7 @@ protected: bool UseGivenPermissionsFile = false; bool UseGivenPermissionsDir = false; bool UseSourcePermissions = true; + bool ExcludeEmptyDirectories = false; bool FollowSymlinkChain = false; std::string Destination; std::string FilesFromDir; diff --git a/Source/cmInstallCommand.cxx b/Source/cmInstallCommand.cxx index e0044c6004..d65d8849d7 100644 --- a/Source/cmInstallCommand.cxx +++ b/Source/cmInstallCommand.cxx @@ -1805,6 +1805,14 @@ bool HandleDirectoryMode(std::vector const& args, } exclude_from_all = true; doing = DoingNone; + } else if (args[i] == "EXCLUDE_EMPTY_DIRECTORIES") { + if (in_match_mode) { + status.SetError(cmStrCat(args[0], " does not allow \"", args[i], + "\" after PATTERN or REGEX.")); + return false; + } + literal_args += " EXCLUDE_EMPTY_DIRECTORIES"; + doing = DoingNone; } else if (doing == DoingDirs) { // Convert this directory to a full path. std::string dir = args[i]; diff --git a/Tests/RunCMake/install/DIRECTORY-EXCLUDE_EMPTY_DIRECTORIES-check.cmake b/Tests/RunCMake/install/DIRECTORY-EXCLUDE_EMPTY_DIRECTORIES-check.cmake new file mode 100644 index 0000000000..f5b505bac7 --- /dev/null +++ b/Tests/RunCMake/install/DIRECTORY-EXCLUDE_EMPTY_DIRECTORIES-check.cmake @@ -0,0 +1,30 @@ +file(REMOVE_RECURSE ${RunCMake_TEST_BINARY_DIR}/prefix) + +execute_process(COMMAND ${CMAKE_COMMAND} -P ${RunCMake_TEST_BINARY_DIR}/cmake_install.cmake + OUTPUT_VARIABLE out ERROR_VARIABLE err) + +set(f ${RunCMake_TEST_BINARY_DIR}/prefix/dir_to_install/empty.txt) +if(NOT EXISTS "${f}") + string(APPEND RunCMake_TEST_FAILED + "File was not installed:\n ${f}\n") +endif() + +set(empty_folder ${RunCMake_TEST_BINARY_DIR}/prefix/dir_to_install/empty_folder) +if(EXISTS "${empty_folder}") + string(APPEND RunCMake_TEST_FAILED + "empty_folder should not have be installed:\n ${empty_folder}\n") +endif() + +if(UNIX) + set(folder_with_symlink ${RunCMake_TEST_BINARY_DIR}/prefix/dir_to_install/folder_with_symlink) + if(NOT EXISTS "${folder_with_symlink}") + string(APPEND RunCMake_TEST_FAILED + "folder_with_symlink was not installed:\n ${folder_with_symlink}\n") + endif() + + set(symlink_to_empty_txt ${RunCMake_TEST_BINARY_DIR}/prefix/dir_to_install/folder_with_symlink/symlink_to_empty.txt) + if(NOT EXISTS "${symlink_to_empty_txt}") + string(APPEND RunCMake_TEST_FAILED + "symlink_to_empty.txt was not installed:\n ${symlink_to_empty_txt}\n") + endif() +endif() diff --git a/Tests/RunCMake/install/DIRECTORY-EXCLUDE_EMPTY_DIRECTORIES.cmake b/Tests/RunCMake/install/DIRECTORY-EXCLUDE_EMPTY_DIRECTORIES.cmake new file mode 100644 index 0000000000..195d82ae21 --- /dev/null +++ b/Tests/RunCMake/install/DIRECTORY-EXCLUDE_EMPTY_DIRECTORIES.cmake @@ -0,0 +1,27 @@ +set(CMAKE_INSTALL_MESSAGE "ALWAYS") +set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/prefix") + +set(DIR_TO_INSTALL "${CMAKE_BINARY_DIR}/dir_to_install") +file(MAKE_DIRECTORY ${DIR_TO_INSTALL}) + +file(TOUCH ${DIR_TO_INSTALL}/empty.txt) + +# make an empty folder +file(MAKE_DIRECTORY ${DIR_TO_INSTALL}/empty_folder) +# make empty subfolders under the empty folder +file(MAKE_DIRECTORY ${DIR_TO_INSTALL}/empty_folder/empty_subfolder1) +file(MAKE_DIRECTORY ${DIR_TO_INSTALL}/empty_folder/empty_subfolder2) + +if(UNIX) + # make an folder with a symlink + file(MAKE_DIRECTORY ${DIR_TO_INSTALL}/folder_with_symlink) + file(CREATE_LINK ${DIR_TO_INSTALL}/empty.txt + ${DIR_TO_INSTALL}/folder_with_symlink/symlink_to_empty.txt + SYMBOLIC + ) +endif() + +install(DIRECTORY ${DIR_TO_INSTALL} + DESTINATION ${CMAKE_INSTALL_PREFIX} + EXCLUDE_EMPTY_DIRECTORIES +) diff --git a/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES-result.txt b/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES-stderr.txt b/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES-stderr.txt new file mode 100644 index 0000000000..7d253c201a --- /dev/null +++ b/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES-stderr.txt @@ -0,0 +1,5 @@ +CMake Error at DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES.cmake:[0-9]+ \(install\): + install DIRECTORY does not allow "EXCLUDE_EMPTY_DIRECTORIES" after PATTERN + or REGEX\. +Call Stack \(most recent call first\): + CMakeLists\.txt:[0-9]+ \(include\) diff --git a/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES.cmake b/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES.cmake new file mode 100644 index 0000000000..d4434f401a --- /dev/null +++ b/Tests/RunCMake/install/DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES.cmake @@ -0,0 +1 @@ +install(DIRECTORY src DESTINATION src PATTERN *.txt EXCLUDE_EMPTY_DIRECTORIES) diff --git a/Tests/RunCMake/install/RunCMakeTest.cmake b/Tests/RunCMake/install/RunCMakeTest.cmake index 3a121c0b0e..85a3e07b7a 100644 --- a/Tests/RunCMake/install/RunCMakeTest.cmake +++ b/Tests/RunCMake/install/RunCMakeTest.cmake @@ -73,6 +73,8 @@ run_cmake(DIRECTORY-MESSAGE_NEVER) run_cmake(DIRECTORY-PATTERN-MESSAGE_NEVER) run_cmake(DIRECTORY-message) run_cmake(DIRECTORY-message-lazy) +run_cmake(DIRECTORY-EXCLUDE_EMPTY_DIRECTORIES) +run_cmake(DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES) run_cmake(SkipInstallRulesWarning) run_cmake(SkipInstallRulesNoWarning1) run_cmake(SkipInstallRulesNoWarning2)