file(CREATE_LINK): Implement COPY_ON_ERROR for directories

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

Fixes: #27294
This commit is contained in:
Hanna Rusakovich
2025-10-15 17:01:08 +03:00
committed by hanna.rusakovich
parent f719a36bc0
commit a73ddd2ddb
20 changed files with 247 additions and 20 deletions

View File

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

View File

@@ -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
Help/policy/CMP0205.rst Normal file
View File

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

View File

@@ -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;
} else {
result = cmStrCat("failed to create link '", newFileName,
"': ", linked.GetString());
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, "': 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}/*")

View File

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

View File

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