file(INSTALL): Add FOLLOW_SYMLINK_CHAIN argument

This commit is contained in:
Kyle Edwards 2019-05-16 15:23:14 -04:00
parent 64a7f491ef
commit e3ff7ced63
6 changed files with 268 additions and 7 deletions

View File

@ -311,6 +311,7 @@ Create the given directories and their parents as needed.
[FILE_PERMISSIONS <permissions>...]
[DIRECTORY_PERMISSIONS <permissions>...]
[NO_SOURCE_PERMISSIONS] [USE_SOURCE_PERMISSIONS]
[FOLLOW_SYMLINK_CHAIN]
[FILES_MATCHING]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS <permissions>...]] [...])
@ -324,6 +325,32 @@ at the destination with the same timestamp. Copying preserves input
permissions unless explicit permissions or ``NO_SOURCE_PERMISSIONS``
are given (default is ``USE_SOURCE_PERMISSIONS``).
If ``FOLLOW_SYMLINK_CHAIN`` is specified, ``COPY`` will recursively resolve
the symlinks at the paths given until a real file is found, and install
a corresponding symlink in the destination for each symlink encountered. For
each symlink that is installed, the resolution is stripped of the directory,
leaving only the filename, meaning that the new symlink points to a file in
the same directory as the symlink. This feature is useful on some Unix systems,
where libraries are installed as a chain of symlinks with version numbers, with
less specific versions pointing to more specific versions.
``FOLLOW_SYMLINK_CHAIN`` will install all of these symlinks and the library
itself into the destination directory. For example, if you have the following
directory structure:
* ``/opt/foo/lib/libfoo.so.1.2.3``
* ``/opt/foo/lib/libfoo.so.1.2 -> libfoo.so.1.2.3``
* ``/opt/foo/lib/libfoo.so.1 -> libfoo.so.1.2``
* ``/opt/foo/lib/libfoo.so -> libfoo.so.1``
and you do:
.. code-block:: cmake
file(COPY /opt/foo/lib/libfoo.so DESTINATION lib FOLLOW_SYMLINK_CHAIN)
This will install all of the symlinks and ``libfoo.so.1.2.3`` itself into
``lib``.
See the :command:`install(DIRECTORY)` command for documentation of
permissions, ``FILES_MATCHING``, ``PATTERN``, ``REGEX``, and
``EXCLUDE`` options. Copying directories preserves the structure

View File

@ -0,0 +1,6 @@
file-install-follow-symlink-chain
---------------------------------
* The :command:`file(INSTALL)` command learned a new argument,
``FOLLOW_SYMLINK_CHAIN``, which can be used to recursively resolve and
install symlinks.

View File

@ -31,6 +31,7 @@ cmFileCopier::cmFileCopier(cmFileCommand* command, const char* name)
, UseGivenPermissionsFile(false)
, UseGivenPermissionsDir(false)
, UseSourcePermissions(true)
, FollowSymlinkChain(false)
, Doing(DoingNone)
{
}
@ -249,6 +250,9 @@ bool cmFileCopier::CheckKeyword(std::string const& arg)
this->Doing = DoingPattern;
} else if (arg == "REGEX") {
this->Doing = DoingRegex;
} else if (arg == "FOLLOW_SYMLINK_CHAIN") {
this->FollowSymlinkChain = true;
this->Doing = DoingNone;
} else if (arg == "EXCLUDE") {
// Add this property to the current match rule.
if (this->CurrentMatchRule) {
@ -464,16 +468,69 @@ bool cmFileCopier::Install(const std::string& fromFile,
if (cmSystemTools::SameFile(fromFile, toFile)) {
return true;
}
if (cmSystemTools::FileIsSymlink(fromFile)) {
return this->InstallSymlink(fromFile, toFile);
std::string newFromFile = fromFile;
std::string newToFile = toFile;
if (this->FollowSymlinkChain &&
!this->InstallSymlinkChain(newFromFile, newToFile)) {
return false;
}
if (cmSystemTools::FileIsDirectory(fromFile)) {
return this->InstallDirectory(fromFile, toFile, match_properties);
if (cmSystemTools::FileIsSymlink(newFromFile)) {
return this->InstallSymlink(newFromFile, newToFile);
}
if (cmSystemTools::FileExists(fromFile)) {
return this->InstallFile(fromFile, toFile, match_properties);
if (cmSystemTools::FileIsDirectory(newFromFile)) {
return this->InstallDirectory(newFromFile, newToFile, match_properties);
}
return this->ReportMissing(fromFile);
if (cmSystemTools::FileExists(newFromFile)) {
return this->InstallFile(newFromFile, newToFile, match_properties);
}
return this->ReportMissing(newFromFile);
}
bool cmFileCopier::InstallSymlinkChain(std::string& fromFile,
std::string& toFile)
{
std::string newFromFile;
std::string toFilePath = cmSystemTools::GetFilenamePath(toFile);
while (cmSystemTools::ReadSymlink(fromFile, newFromFile)) {
if (!cmSystemTools::FileIsFullPath(newFromFile)) {
std::string fromFilePath = cmSystemTools::GetFilenamePath(fromFile);
newFromFile = fromFilePath + "/" + newFromFile;
}
std::string symlinkTarget = cmSystemTools::GetFilenameName(newFromFile);
bool copy = true;
if (!this->Always) {
std::string oldSymlinkTarget;
if (cmSystemTools::ReadSymlink(toFile, oldSymlinkTarget)) {
if (symlinkTarget == oldSymlinkTarget) {
copy = false;
}
}
}
this->ReportCopy(toFile, TypeLink, copy);
if (copy) {
cmSystemTools::RemoveFile(toFile);
cmSystemTools::MakeDirectory(toFilePath);
if (!cmSystemTools::CreateSymlink(symlinkTarget, toFile)) {
std::ostringstream e;
e << this->Name << " cannot create symlink \"" << toFile << "\".";
this->FileCommand->SetError(e.str());
return false;
}
}
fromFile = newFromFile;
toFile = toFilePath + "/" + symlinkTarget;
}
return true;
}
bool cmFileCopier::InstallSymlink(const std::string& fromFile,

View File

@ -64,6 +64,7 @@ protected:
// Translate an argument to a permissions bit.
bool CheckPermissions(std::string const& arg, mode_t& permissions);
bool InstallSymlinkChain(std::string& fromFile, std::string& toFile);
bool InstallSymlink(const std::string& fromFile, const std::string& toFile);
bool InstallFile(const std::string& fromFile, const std::string& toFile,
MatchProperties match_properties);
@ -86,6 +87,7 @@ protected:
bool UseGivenPermissionsFile;
bool UseGivenPermissionsDir;
bool UseSourcePermissions;
bool FollowSymlinkChain;
std::string Destination;
std::string FilesFromDir;
std::vector<std::string> Files;

View File

@ -0,0 +1,168 @@
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/dest1")
file(TOUCH "${CMAKE_BINARY_DIR}/file1.txt")
file(CREATE_LINK file1.txt "${CMAKE_BINARY_DIR}/file1.txt.sym" SYMBOLIC)
file(TOUCH "${CMAKE_BINARY_DIR}/dest1/file1.txt.sym")
file(TOUCH "${CMAKE_BINARY_DIR}/file2.txt")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/file2")
file(CREATE_LINK ../file2.txt "${CMAKE_BINARY_DIR}/file2/file2.txt.sym" SYMBOLIC)
file(TOUCH "${CMAKE_BINARY_DIR}/file3.txt")
file(CREATE_LINK "${CMAKE_BINARY_DIR}/file3.txt" "${CMAKE_BINARY_DIR}/file3.txt.sym" SYMBOLIC)
file(TOUCH "${CMAKE_BINARY_DIR}/file4.txt")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/file4")
file(CREATE_LINK "${CMAKE_BINARY_DIR}/file4.txt" "${CMAKE_BINARY_DIR}/file4/file4.txt.sym" SYMBOLIC)
file(TOUCH "${CMAKE_BINARY_DIR}/file5.txt")
file(TOUCH "${CMAKE_BINARY_DIR}/file6.txt")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/file6/file6")
file(CREATE_LINK file6.txt "${CMAKE_BINARY_DIR}/file6.txt.sym.1" SYMBOLIC)
file(CREATE_LINK ../file6.txt.sym.1 "${CMAKE_BINARY_DIR}/file6/file6.txt.sym.2" SYMBOLIC)
file(CREATE_LINK "${CMAKE_BINARY_DIR}/file6/file6.txt.sym.2" "${CMAKE_BINARY_DIR}/file6/file6/file6.txt.sym.3" SYMBOLIC)
file(CREATE_LINK file6.txt.sym.3 "${CMAKE_BINARY_DIR}/file6/file6/file6.txt.sym.4" SYMBOLIC)
file(TOUCH "${CMAKE_BINARY_DIR}/file7.txt")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/file7")
file(TOUCH "${CMAKE_BINARY_DIR}/file8.txt")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/file8")
file(CREATE_LINK "${CMAKE_BINARY_DIR}/file8/../file8.txt" "${CMAKE_BINARY_DIR}/file8/file8.txt.sym" SYMBOLIC)
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/file9")
file(TOUCH "${CMAKE_BINARY_DIR}/file9/file9.txt")
file(CREATE_LINK "${CMAKE_BINARY_DIR}/file9" "${CMAKE_BINARY_DIR}/file9.sym" SYMBOLIC)
file(TOUCH "${CMAKE_BINARY_DIR}/file10.txt")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/file10")
file(CREATE_LINK "." "${CMAKE_BINARY_DIR}/file10/file10" SYMBOLIC)
file(CREATE_LINK "${CMAKE_BINARY_DIR}/file10/file10/../file10.txt" "${CMAKE_BINARY_DIR}/file10/file10.txt.sym" SYMBOLIC)
file(INSTALL
"${CMAKE_BINARY_DIR}/file1.txt.sym"
DESTINATION "${CMAKE_BINARY_DIR}/dest1"
FOLLOW_SYMLINK_CHAIN
)
file(INSTALL
"${CMAKE_BINARY_DIR}/file1.txt.sym"
"${CMAKE_BINARY_DIR}/file2/file2.txt.sym"
"${CMAKE_BINARY_DIR}/file3.txt.sym"
"${CMAKE_BINARY_DIR}/file4/file4.txt.sym"
"${CMAKE_BINARY_DIR}/file5.txt"
"${CMAKE_BINARY_DIR}/file6/file6/file6.txt.sym.4"
"${CMAKE_BINARY_DIR}/file8/file8.txt.sym"
"${CMAKE_BINARY_DIR}/file7/../file7.txt"
"${CMAKE_BINARY_DIR}/file8.txt"
"${CMAKE_BINARY_DIR}/file9.sym/file9.txt"
"${CMAKE_BINARY_DIR}/file10/file10/file10.txt.sym"
DESTINATION "${CMAKE_BINARY_DIR}/dest2"
FOLLOW_SYMLINK_CHAIN
)
set(resolved_file1.txt.sym file1.txt)
set(resolved_file10.txt.sym file10.txt)
set(resolved_file2.txt.sym file2.txt)
set(resolved_file3.txt.sym file3.txt)
set(resolved_file4.txt.sym file4.txt)
set(resolved_file6.txt.sym.1 file6.txt)
set(resolved_file6.txt.sym.2 file6.txt.sym.1)
set(resolved_file6.txt.sym.3 file6.txt.sym.2)
set(resolved_file6.txt.sym.4 file6.txt.sym.3)
set(resolved_file8.txt.sym file8.txt)
set(syms)
foreach(f
file1.txt
file1.txt.sym
file10.txt
file10.txt.sym
file2.txt
file2.txt.sym
file3.txt
file3.txt.sym
file4.txt
file4.txt.sym
file5.txt
file6.txt
file6.txt.sym.1
file6.txt.sym.2
file6.txt.sym.3
file6.txt.sym.4
file7.txt
file8.txt
file8.txt.sym
file9.txt
)
string(REPLACE "." "\\." r "${f}")
list(APPEND syms "[^;]*/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN-build/dest2/${r}")
set(filename "${CMAKE_BINARY_DIR}/dest2/${f}")
if(DEFINED resolved_${f})
file(READ_SYMLINK "${filename}" resolved)
if(NOT resolved STREQUAL "${resolved_${f}}")
message(SEND_ERROR "Expected symlink resolution for ${f}: ${resolved_${f}}\nActual resolution: ${resolved}")
endif()
elseif(NOT EXISTS "${filename}" OR IS_SYMLINK "${filename}" OR IS_DIRECTORY "${filename}")
message(SEND_ERROR "${f} should be a regular file")
endif()
endforeach()
file(GLOB_RECURSE actual_syms LIST_DIRECTORIES true "${CMAKE_BINARY_DIR}/dest2/*")
if(NOT actual_syms MATCHES "^${syms}$")
message(SEND_ERROR "Expected files:\n\n ^${syms}$\n\nActual files:\n\n ${actual_syms}")
endif()
file(INSTALL
"${CMAKE_BINARY_DIR}/file1.txt.sym"
"${CMAKE_BINARY_DIR}/file2/file2.txt.sym"
"${CMAKE_BINARY_DIR}/file3.txt.sym"
"${CMAKE_BINARY_DIR}/file4/file4.txt.sym"
"${CMAKE_BINARY_DIR}/file5.txt"
"${CMAKE_BINARY_DIR}/file6/file6/file6.txt.sym.4"
"${CMAKE_BINARY_DIR}/file8/file8.txt.sym"
"${CMAKE_BINARY_DIR}/file7/../file7.txt"
"${CMAKE_BINARY_DIR}/file8.txt"
"${CMAKE_BINARY_DIR}/file9.sym/file9.txt"
"${CMAKE_BINARY_DIR}/file10/file10/file10.txt.sym"
DESTINATION "${CMAKE_BINARY_DIR}/dest3"
)
set(resolved_file1.txt.sym [[^file1\.txt$]])
set(resolved_file10.txt.sym [[/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN-build/file10/file10/\.\./file10\.txt$]])
set(resolved_file2.txt.sym [[^\.\./file2\.txt$]])
set(resolved_file3.txt.sym [[/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN-build/file3\.txt$]])
set(resolved_file4.txt.sym [[/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN-build/file4\.txt$]])
set(resolved_file6.txt.sym.4 [[^file6\.txt\.sym\.3$]])
set(resolved_file8.txt.sym [[/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN-build/file8/\.\./file8\.txt$]])
set(syms)
foreach(f
file1.txt.sym
file10.txt.sym
file2.txt.sym
file3.txt.sym
file4.txt.sym
file5.txt
file6.txt.sym.4
file7.txt
file8.txt
file8.txt.sym
file9.txt
)
string(REPLACE "." "\\." r "${f}")
list(APPEND syms "[^;]*/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN-build/dest3/${r}")
set(filename "${CMAKE_BINARY_DIR}/dest3/${f}")
if(DEFINED resolved_${f})
file(READ_SYMLINK "${filename}" resolved)
if(NOT resolved MATCHES "${resolved_${f}}")
message(SEND_ERROR "Expected symlink resolution for ${f}: ${resolved_${f}}\nActual resolution: ${resolved}")
endif()
elseif(NOT EXISTS "${filename}" OR IS_SYMLINK "${filename}" OR IS_DIRECTORY "${filename}")
message(SEND_ERROR "${f} should be a regular file")
endif()
endforeach()
file(GLOB_RECURSE actual_syms LIST_DIRECTORIES true "${CMAKE_BINARY_DIR}/dest3/*")
if(NOT actual_syms MATCHES "^${syms}$")
message(SEND_ERROR "Expected files:\n\n ^${syms}$\n\nActual files:\n\n ${actual_syms}")
endif()

View File

@ -64,6 +64,7 @@ if(NOT WIN32 OR CYGWIN)
run_cmake(READ_SYMLINK)
run_cmake(READ_SYMLINK-noexist)
run_cmake(READ_SYMLINK-notsymlink)
run_cmake(INSTALL-FOLLOW_SYMLINK_CHAIN)
endif()
if(RunCMake_GENERATOR STREQUAL "Ninja")