瀏覽代碼

file(CREATE_LINK): Implement COPY_ON_ERROR for directories

Add policy `CMP0205` for compatibility with projects not expecting this.

Fixes: #27294
Hanna Rusakovich 1 月之前
父節點
當前提交
a73ddd2ddb

+ 4 - 3
Help/command/file.rst

@@ -615,11 +615,12 @@ Filesystem
   emitted.
 
   Specifying ``COPY_ON_ERROR`` enables copying the file as a fallback if
-  creating the link fails.  If the source is a directory, the destination
-  directory will be created if it does not exist, but no files will be copied
-  the from source one.  It can be useful for handling situations such as
+  creating the link fails.  It can be useful for handling situations such as
   ``<original>`` and ``<linkname>`` being on different drives or mount points,
   which would make them unable to support a hard link.
+  If the source is a directory, the destination directory will be created if
+  it does not exist.  Contents of the source directory will be copied to the
+  destination directory unless policy :policy:`CMP0205` is not set to ``NEW``.
 
 .. signature::
   file(CHMOD <files>... <directories>...

+ 8 - 0
Help/manual/cmake-policies.7.rst

@@ -92,6 +92,14 @@ Supported Policies
 
 The following policies are supported.
 
+Policies Introduced by CMake 4.3
+--------------------------------
+
+.. toctree::
+   :maxdepth: 1
+
+   CMP0205: file(CREATE_LINK) with COPY_ON_ERROR copies directory content. </policy/CMP0205>
+
 Policies Introduced by CMake 4.2
 --------------------------------
 

+ 23 - 0
Help/policy/CMP0205.rst

@@ -0,0 +1,23 @@
+CMP0205
+-------
+
+.. versionadded:: 4.3
+
+:command:`file(CREATE_LINK)` with ``COPY_ON_ERROR`` copies directory content.
+
+The :command:`file(CREATE_LINK)` command's ``COPY_ON_ERROR`` option copies
+the source file to the destination as a fallback if linking it fails.
+If the source is a directory, CMake 4.2 and below create the destination
+directory but do not copy its contents.  CMake 4.3 and above prefer to
+copy the directory contents too.  This policy provides compatibility with
+projects that have not been updated to expect the contents to be copied.
+
+The ``OLD`` behavior for this policy is to create the destination directory
+without copying contents.  The ``NEW`` behavior for this policy to create
+the destination directory and copy contents from the source directory.
+
+.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 4.3
+.. |WARNS_OR_DOES_NOT_WARN| replace:: warns
+.. include:: include/STANDARD_ADVICE.rst
+
+.. include:: include/DEPRECATED.rst

+ 60 - 13
Source/cmFileCommand.cxx

@@ -3221,8 +3221,13 @@ bool HandleCreateLinkCommand(std::vector<std::string> const& args,
     return false;
   }
 
+  cmPolicies::PolicyStatus const cmp0205 =
+    status.GetMakefile().GetPolicyStatus(cmPolicies::CMP0205);
+
   // Hard link requires original file to exist.
-  if (!arguments.Symbolic && !cmSystemTools::FileExists(fileName)) {
+  if (!arguments.Symbolic &&
+      (!cmSystemTools::PathExists(fileName) ||
+       (cmp0205 != cmPolicies::NEW && !cmSystemTools::FileExists(fileName)))) {
     result = "Cannot hard link \'" + fileName + "\' as it does not exist.";
     if (!arguments.Result.empty()) {
       status.GetMakefile().AddDefinition(arguments.Result, result);
@@ -3234,11 +3239,17 @@ bool HandleCreateLinkCommand(std::vector<std::string> const& args,
 
   // Check if the new file already exists and remove it.
   if (cmSystemTools::PathExists(newFileName)) {
-    cmsys::Status rmStatus = cmSystemTools::RemoveFile(newFileName);
+    cmsys::Status rmStatus;
+    if (cmp0205 == cmPolicies::NEW &&
+        cmSystemTools::FileIsDirectory(newFileName)) {
+      rmStatus = cmSystemTools::RepeatedRemoveDirectory(newFileName);
+    } else {
+      rmStatus = cmSystemTools::RemoveFile(newFileName);
+    }
     if (!rmStatus) {
-      auto err = cmStrCat("Failed to create link '", newFileName,
-                          "' because existing path cannot be removed: ",
-                          rmStatus.GetString(), '\n');
+      std::string err = cmStrCat("Failed to create link '", newFileName,
+                                 "' because existing path cannot be removed: ",
+                                 rmStatus.GetString(), '\n');
 
       if (!arguments.Result.empty()) {
         status.GetMakefile().AddDefinition(arguments.Result, err);
@@ -3249,6 +3260,8 @@ bool HandleCreateLinkCommand(std::vector<std::string> const& args,
     }
   }
 
+  bool const sourceIsDirectory = cmSystemTools::FileIsDirectory(fileName);
+
   // Whether the operation completed successfully.
   bool completed = false;
 
@@ -3263,20 +3276,54 @@ bool HandleCreateLinkCommand(std::vector<std::string> const& args,
                         "': ", linked.GetString());
     }
   } else {
-    cmsys::Status linked =
-      cmSystemTools::CreateLinkQuietly(fileName, newFileName);
-    if (linked) {
-      completed = true;
+    bool needToTry = true;
+    if (sourceIsDirectory) {
+      if (cmp0205 == cmPolicies::NEW) {
+        needToTry = false;
+      } else if (cmp0205 == cmPolicies::WARN) {
+        status.GetMakefile().IssueMessage(
+          MessageType::AUTHOR_WARNING,
+          cmStrCat("Path\n  ", fileName,
+                   "\nis directory. Hardlinks creation is not supported for "
+                   "directories.\n",
+                   cmPolicies::GetPolicyWarning(cmPolicies::CMP0205)));
+      }
+    }
+
+    if (needToTry) {
+      cmsys::Status linked =
+        cmSystemTools::CreateLinkQuietly(fileName, newFileName);
+      if (linked) {
+        completed = true;
+      } else {
+        result = cmStrCat("failed to create link '", newFileName,
+                          "': ", linked.GetString());
+      }
     } else {
-      result = cmStrCat("failed to create link '", newFileName,
-                        "': ", linked.GetString());
+      result =
+        cmStrCat("failed to create link '", newFileName, "': not supported");
     }
   }
 
+  if (arguments.CopyOnError && cmp0205 == cmPolicies::WARN &&
+      sourceIsDirectory) {
+    status.GetMakefile().IssueMessage(
+      MessageType::AUTHOR_WARNING,
+      cmStrCat("Path\n  ", fileName,
+               "\nis directory. It will be copied recursively when NEW policy "
+               "behavior applies for CMP0205.\n",
+               cmPolicies::GetPolicyWarning(cmPolicies::CMP0205)));
+  }
+
   // Check if copy-on-error is enabled in the arguments.
   if (!completed && arguments.CopyOnError) {
-    cmsys::Status copied =
-      cmsys::SystemTools::CopyFileAlways(fileName, newFileName);
+    cmsys::Status copied;
+    if (cmp0205 == cmPolicies::NEW && sourceIsDirectory) {
+      copied = cmsys::SystemTools::CopyADirectory(fileName, newFileName);
+    } else {
+      copied = cmsys::SystemTools::CopyFileAlways(fileName, newFileName);
+    }
+
     if (copied) {
       completed = true;
     } else {

+ 4 - 1
Source/cmPolicies.h

@@ -612,7 +612,10 @@ class cmMakefile;
          4, 2, 0, WARN)                                                       \
   SELECT(POLICY, CMP0204,                                                     \
          "A character set is always defined when targeting the MSVC ABI.", 4, \
-         2, 0, WARN)
+         2, 0, WARN)                                                          \
+  SELECT(POLICY, CMP0205,                                                     \
+         "file(CREATE_LINK) with COPY_ON_ERROR copies directory content.", 4, \
+         3, 0, WARN)
 
 #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
 #define CM_FOR_EACH_POLICY_ID(POLICY)                                         \

+ 3 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-HardLink-NEW.cmake

@@ -0,0 +1,3 @@
+set(link_name HardLink)
+set(maybe_SYMBOLIC)
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common-NEW.cmake")

+ 3 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-HardLink-OLD.cmake

@@ -0,0 +1,3 @@
+set(link_name HardLink)
+set(maybe_SYMBOLIC)
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common-OLD.cmake")

+ 38 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-HardLink-WARN-stderr.txt

@@ -0,0 +1,38 @@
+^CMake Warning \(dev\) at [^
+]*/CMP0205-common\.cmake:[0-9]+ \(file\):
+  Path
+
+    [^
+]*[\\|/]file-CREATE_LINK[\\|/]CMP0205
+
+  is directory.  Hardlinks creation is not supported for directories.
+
+  Policy CMP0205 is not set: file\(CREATE_LINK\) with COPY_ON_ERROR copies
+  directory content\.  Run "cmake --help-policy CMP0205" for policy details\.
+  Use the cmake_policy command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  [^
+]*/CMP0205-common-WARN\.cmake:[0-9]+ \(include\)
+  [^
+]*/CMP0205-HardLink-WARN\.cmake:[0-9]+ \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.
+
+CMake Warning \(dev\) at [^
+]*/CMP0205-common\.cmake:[0-9]+ \(file\):
+  Path
+
+    [^
+]*[\\|/]file-CREATE_LINK[\\|/]CMP0205
+
+  is directory.  It will be copied recursively when NEW policy behavior
+  applies for CMP0205\.
+
+  Policy CMP0205 is not set: file\(CREATE_LINK\) with COPY_ON_ERROR copies
+  directory content\.  Run "cmake --help-policy CMP0205" for policy details\.
+  Use the cmake_policy command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  [^
+]*/CMP0205-common-WARN\.cmake:[0-9]+ \(include\)
+  [^
+]*/CMP0205-HardLink-WARN\.cmake:[0-9]+ \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.$

+ 3 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-HardLink-WARN.cmake

@@ -0,0 +1,3 @@
+set(link_name HardLink)
+set(maybe_SYMBOLIC)
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common-WARN.cmake")

+ 3 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-SymLink-NEW.cmake

@@ -0,0 +1,3 @@
+set(link_name SymLink)
+set(maybe_SYMBOLIC SYMBOLIC)
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common-NEW.cmake")

+ 3 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-SymLink-OLD.cmake

@@ -0,0 +1,3 @@
+set(link_name SymLink)
+set(maybe_SYMBOLIC SYMBOLIC)
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common-OLD.cmake")

+ 19 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-SymLink-WARN-stderr.txt

@@ -0,0 +1,19 @@
+^CMake Warning \(dev\) at [^
+]*/CMP0205-common\.cmake:[0-9]+ \(file\):
+  Path
+
+    [^
+]*[\\|/]file-CREATE_LINK[\\|/]CMP0205
+
+  is directory.  It will be copied recursively when NEW policy behavior
+  applies for CMP0205\.
+
+  Policy CMP0205 is not set: file\(CREATE_LINK\) with COPY_ON_ERROR copies
+  directory content\.  Run "cmake --help-policy CMP0205" for policy details\.
+  Use the cmake_policy command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  [^
+]*/CMP0205-common-WARN\.cmake:[0-9]+ \(include\)
+  [^
+]*/CMP0205-SymLink-WARN\.cmake:[0-9]+ \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.$

+ 3 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-SymLink-WARN.cmake

@@ -0,0 +1,3 @@
+set(link_name SymLink)
+set(maybe_SYMBOLIC SYMBOLIC)
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common-WARN.cmake")

+ 12 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-common-NEW.cmake

@@ -0,0 +1,12 @@
+cmake_policy(SET CMP0205 NEW)
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common.cmake")
+
+if(NOT allFilesDst)
+  message(SEND_ERROR "Destination directory is empty: '${allFilesDst}'")
+endif()
+
+if(NOT "${allFilesSrc}" STREQUAL "${allFilesDst}")
+  message(SEND_ERROR "Source and destination directories are not equal")
+  message(SEND_ERROR "Source files: '${allFilesSrc}'")
+  message(SEND_ERROR "Destination files: '${allFilesDst}'")
+endif()

+ 6 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-common-OLD.cmake

@@ -0,0 +1,6 @@
+cmake_policy(SET CMP0205 OLD)
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common.cmake")
+
+if(allFilesDst)
+  message(SEND_ERROR "Directory is not empty: '${allFilesDst}'")
+endif()

+ 6 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-common-WARN.cmake

@@ -0,0 +1,6 @@
+# CMP0205 is unset
+include("${CMAKE_CURRENT_LIST_DIR}/CMP0205-common.cmake")
+
+if(allFilesDst)
+  message(SEND_ERROR "Directory is not empty: '${allFilesDst}'")
+endif()

+ 14 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205-common.cmake

@@ -0,0 +1,14 @@
+# Use COPY_ON_ERROR to handle the case where the source and destination
+# directory are on different devices and empty.
+file(CREATE_LINK
+  ${CMAKE_CURRENT_LIST_DIR}/CMP0205 ${CMAKE_CURRENT_BINARY_DIR}/CMP0205-${link_name}
+  ${maybe_SYMBOLIC}
+  RESULT result
+  COPY_ON_ERROR
+  )
+if(NOT result STREQUAL "0")
+  message(SEND_ERROR "COPY_ON_ERROR failed: '${result}'")
+endif()
+
+file(GLOB_RECURSE allFilesSrc LIST_DIRECTORIES true RELATIVE "${CMAKE_CURRENT_LIST_DIR}/CMP0205" "${CMAKE_CURRENT_LIST_DIR}/CMP0205/*")
+file(GLOB_RECURSE allFilesDst LIST_DIRECTORIES true RELATIVE "${CMAKE_CURRENT_BINARY_DIR}/CMP0205-${link_name}" "${CMAKE_CURRENT_BINARY_DIR}/CMP0205-${link_name}/*")

+ 0 - 0
Tests/RunCMake/file-CREATE_LINK/CMP0205/test.txt


+ 2 - 1
Tests/RunCMake/file-CREATE_LINK/CREATE_LINK-COPY_ON_ERROR.cmake → Tests/RunCMake/file-CREATE_LINK/CREATE_LINK-COPY_ON_ERROR-file.cmake

@@ -1,6 +1,7 @@
 # Use COPY_ON_ERROR to handle the case where the source and destination
-# directory are on different devices. Cross-device links are not permitted
+# file are on different devices. Cross-device links are not permitted
 # and the following command falls back to copying the file if link fails.
+# Check only command result.
 file(CREATE_LINK
   ${CMAKE_CURRENT_LIST_FILE} TestCreateLink.cmake
   RESULT result

+ 32 - 1
Tests/RunCMake/file-CREATE_LINK/RunCMakeTest.cmake

@@ -1,7 +1,7 @@
 include(RunCMake)
 
 run_cmake(CREATE_LINK)
-run_cmake(CREATE_LINK-COPY_ON_ERROR)
+run_cmake(CREATE_LINK-COPY_ON_ERROR-file)
 run_cmake(CREATE_LINK-noarg)
 run_cmake(CREATE_LINK-noexist)
 
@@ -11,3 +11,34 @@ if(NOT WIN32
   run_cmake(CREATE_LINK-SYMBOLIC)
   run_cmake(CREATE_LINK-SYMBOLIC-noexist)
 endif()
+
+file(MAKE_DIRECTORY ${RunCMake_BINARY_DIR}/CMP0205-Inspect/Dest)
+
+file(REMOVE_RECURSE ${RunCMake_BINARY_DIR}/CMP0205-Inspect-SymLink)
+file(CREATE_LINK
+  ${RunCMake_BINARY_DIR}/CMP0205-Inspect/Dest ${RunCMake_BINARY_DIR}/CMP0205-Inspect-SymLink
+  SYMBOLIC
+  RESULT SymLink_RESULT
+)
+if(SymLink_RESULT STREQUAL "0")
+  message(STATUS "CMP0205-SymLink-* skipped: directory symbolic link creation works")
+  file(REMOVE ${RunCMake_BINARY_DIR}/CMP0205-Inspect-SymLink)
+else()
+  run_cmake_script(CMP0205-SymLink-WARN)
+  run_cmake_script(CMP0205-SymLink-OLD)
+  run_cmake_script(CMP0205-SymLink-NEW)
+endif()
+
+file(REMOVE_RECURSE ${RunCMake_BINARY_DIR}/CMP0205-Inspect-HardLink)
+file(CREATE_LINK
+  ${RunCMake_BINARY_DIR}/CMP0205-Inspect/Dest ${RunCMake_BINARY_DIR}/CMP0205-Inspect-HardLink
+  RESULT HardLink_RESULT
+)
+if(HardLink_RESULT STREQUAL "0")
+  message(STATUS "CMP0205-HardLink-* skipped: directory hard link creation works")
+  file(REMOVE_RECURSE ${RunCMake_BINARY_DIR}/CMP0205-Inspect-HardLink)
+else()
+  run_cmake_script(CMP0205-HardLink-WARN)
+  run_cmake_script(CMP0205-HardLink-OLD)
+  run_cmake_script(CMP0205-HardLink-NEW)
+endif()