From e3ff7ced630808e2e74f0853a720bc90d3f35abb Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Thu, 16 May 2019 15:23:14 -0400 Subject: file(INSTALL): Add FOLLOW_SYMLINK_CHAIN argument --- Help/command/file.rst | 27 ++++ .../dev/file-install-follow-symlink-chain.rst | 6 + Source/cmFileCopier.cxx | 71 ++++++++- Source/cmFileCopier.h | 2 + .../file/INSTALL-FOLLOW_SYMLINK_CHAIN.cmake | 168 +++++++++++++++++++++ Tests/RunCMake/file/RunCMakeTest.cmake | 1 + 6 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 Help/release/dev/file-install-follow-symlink-chain.rst create mode 100644 Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN.cmake diff --git a/Help/command/file.rst b/Help/command/file.rst index 465e567..0664e7c 100644 --- a/Help/command/file.rst +++ b/Help/command/file.rst @@ -311,6 +311,7 @@ Create the given directories and their parents as needed. [FILE_PERMISSIONS ...] [DIRECTORY_PERMISSIONS ...] [NO_SOURCE_PERMISSIONS] [USE_SOURCE_PERMISSIONS] + [FOLLOW_SYMLINK_CHAIN] [FILES_MATCHING] [[PATTERN | REGEX ] [EXCLUDE] [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 diff --git a/Help/release/dev/file-install-follow-symlink-chain.rst b/Help/release/dev/file-install-follow-symlink-chain.rst new file mode 100644 index 0000000..8d22512 --- /dev/null +++ b/Help/release/dev/file-install-follow-symlink-chain.rst @@ -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. diff --git a/Source/cmFileCopier.cxx b/Source/cmFileCopier.cxx index 8913e6d..972cd6e 100644 --- a/Source/cmFileCopier.cxx +++ b/Source/cmFileCopier.cxx @@ -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, diff --git a/Source/cmFileCopier.h b/Source/cmFileCopier.h index 003b8f6..a79a60b 100644 --- a/Source/cmFileCopier.h +++ b/Source/cmFileCopier.h @@ -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 Files; diff --git a/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN.cmake b/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN.cmake new file mode 100644 index 0000000..d8a12eb --- /dev/null +++ b/Tests/RunCMake/file/INSTALL-FOLLOW_SYMLINK_CHAIN.cmake @@ -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() diff --git a/Tests/RunCMake/file/RunCMakeTest.cmake b/Tests/RunCMake/file/RunCMakeTest.cmake index 128e8f3..996d1c5 100644 --- a/Tests/RunCMake/file/RunCMakeTest.cmake +++ b/Tests/RunCMake/file/RunCMakeTest.cmake @@ -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") -- cgit v0.12