1
0
mirror of https://github.com/Kitware/CMake.git synced 2025-10-16 05:26:58 +08:00

install(DIRECTORY): Add EXCLUDE_EMPTY_DIRECTORIES option

EXCLUDE_EMPTY_DIRECTORIES option excludes empty directories under the
directory to install. A directory is considered not empty if and only if
the directory contains at least one file or one symbolic link or one
none-empty sub-directory.

Closes: #19189
This commit is contained in:
Hao Dong
2025-04-20 11:17:19 +08:00
committed by Brad King
parent 2a4c5923ef
commit b70ef48b27
11 changed files with 130 additions and 0 deletions

View File

@@ -648,6 +648,7 @@ Signatures
[USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER] [USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
[CONFIGURATIONS <config>...] [CONFIGURATIONS <config>...]
[COMPONENT <component>] [EXCLUDE_FROM_ALL] [COMPONENT <component>] [EXCLUDE_FROM_ALL]
[EXCLUDE_EMPTY_DIRECTORIES]
[FILES_MATCHING] [FILES_MATCHING]
[<match-rule> <match-option>...]... [<match-rule> <match-option>...]...
) )
@@ -750,6 +751,13 @@ Signatures
Disable file installation status output. 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`` ``FILES_MATCHING``
This option may be given before the first ``<match-rule>`` to This option may be given before the first ``<match-rule>`` to
disable installation of files (but not directories) not matched disable installation of files (but not directories) not matched

View File

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

View File

@@ -25,6 +25,10 @@
#include <cstring> #include <cstring>
#include <sstream> #include <sstream>
#include <utility>
#include <cm/string_view>
#include <cmext/string_view>
using namespace cmFSPermissions; using namespace cmFSPermissions;
@@ -294,6 +298,13 @@ bool cmFileCopier::CheckKeyword(std::string const& arg)
this->Doing = DoingNone; this->Doing = DoingNone;
this->MatchlessFiles = false; this->MatchlessFiles = false;
} }
} else if (arg == "EXCLUDE_EMPTY_DIRECTORIES") {
if (this->CurrentMatchRule) {
this->NotAfterMatch(arg);
} else {
this->Doing = DoingNone;
this->ExcludeEmptyDirectories = true;
}
} else { } else {
return false; return false;
} }
@@ -647,6 +658,29 @@ bool cmFileCopier::InstallFile(std::string const& fromFile,
return this->SetPermissions(toFile, permissions); return this->SetPermissions(toFile, permissions);
} }
static bool IsEmptyDirectory(std::string const& path,
std::unordered_map<std::string, bool>& 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, bool cmFileCopier::InstallDirectory(std::string const& source,
std::string const& destination, std::string const& destination,
MatchProperties match_properties) MatchProperties match_properties)
@@ -719,6 +753,11 @@ bool cmFileCopier::InstallDirectory(std::string const& source,
strcmp(dir.GetFile(fileNum), "..") == 0)) { strcmp(dir.GetFile(fileNum), "..") == 0)) {
std::string fromPath = cmStrCat(source, '/', dir.GetFile(fileNum)); std::string fromPath = cmStrCat(source, '/', dir.GetFile(fileNum));
std::string toPath = cmStrCat(destination, '/', dir.GetFile(fileNum)); std::string toPath = cmStrCat(destination, '/', dir.GetFile(fileNum));
if (this->ExcludeEmptyDirectories &&
IsEmptyDirectory(fromPath, this->DirEmptyCache)) {
continue;
}
if (!this->Install(fromPath, toPath)) { if (!this->Install(fromPath, toPath)) {
return false; return false;
} }

View File

@@ -5,6 +5,7 @@
#include "cmConfigure.h" // IWYU pragma: keep #include "cmConfigure.h" // IWYU pragma: keep
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
#include "cmsys/RegularExpression.hxx" #include "cmsys/RegularExpression.hxx"
@@ -30,6 +31,7 @@ protected:
char const* Name; char const* Name;
bool Always = false; bool Always = false;
cmFileTimeCache FileTimes; cmFileTimeCache FileTimes;
std::unordered_map<std::string, bool> DirEmptyCache;
// Whether to install a file not matching any expression. // Whether to install a file not matching any expression.
bool MatchlessFiles = true; bool MatchlessFiles = true;
@@ -89,6 +91,7 @@ protected:
bool UseGivenPermissionsFile = false; bool UseGivenPermissionsFile = false;
bool UseGivenPermissionsDir = false; bool UseGivenPermissionsDir = false;
bool UseSourcePermissions = true; bool UseSourcePermissions = true;
bool ExcludeEmptyDirectories = false;
bool FollowSymlinkChain = false; bool FollowSymlinkChain = false;
std::string Destination; std::string Destination;
std::string FilesFromDir; std::string FilesFromDir;

View File

@@ -1805,6 +1805,14 @@ bool HandleDirectoryMode(std::vector<std::string> const& args,
} }
exclude_from_all = true; exclude_from_all = true;
doing = DoingNone; 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) { } else if (doing == DoingDirs) {
// Convert this directory to a full path. // Convert this directory to a full path.
std::string dir = args[i]; std::string dir = args[i];

View File

@@ -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()

View File

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

View File

@@ -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\)

View File

@@ -0,0 +1 @@
install(DIRECTORY src DESTINATION src PATTERN *.txt EXCLUDE_EMPTY_DIRECTORIES)

View File

@@ -73,6 +73,8 @@ run_cmake(DIRECTORY-MESSAGE_NEVER)
run_cmake(DIRECTORY-PATTERN-MESSAGE_NEVER) run_cmake(DIRECTORY-PATTERN-MESSAGE_NEVER)
run_cmake(DIRECTORY-message) run_cmake(DIRECTORY-message)
run_cmake(DIRECTORY-message-lazy) run_cmake(DIRECTORY-message-lazy)
run_cmake(DIRECTORY-EXCLUDE_EMPTY_DIRECTORIES)
run_cmake(DIRECTORY-PATTERN-EXCLUDE_EMPTY_DIRECTORIES)
run_cmake(SkipInstallRulesWarning) run_cmake(SkipInstallRulesWarning)
run_cmake(SkipInstallRulesNoWarning1) run_cmake(SkipInstallRulesNoWarning1)
run_cmake(SkipInstallRulesNoWarning2) run_cmake(SkipInstallRulesNoWarning2)