From 95323c90a1a7599272d4e2a074d3ab4c856095cd Mon Sep 17 00:00:00 2001
From: Asit Dhal <dhal.asitk@gmail.com>
Date: Sun, 16 Jun 2024 19:44:03 +0200
Subject: file(MAKE_DIRECTORY): Add optional RESULT keyword to capture failure.

Fixes: #26041
---
 Help/command/file.rst                              | 11 +++-
 Help/dev/make_directory-optional-result.rst        |  5 ++
 Source/cmFileCommand.cxx                           | 69 +++++++++++++++++++---
 Tests/RunCMake/CMakeLists.txt                      |  1 +
 Tests/RunCMake/file-MAKE_DIRECTORY/CMakeLists.txt  |  3 +
 ...MAKE_DIRECTORY-Result-many-dirs-FAIL-stdout.txt |  3 +
 .../MAKE_DIRECTORY-Result-many-dirs-FAIL.cmake     |  9 +++
 ...E_DIRECTORY-Result-many-dirs-SUCCESS-stdout.txt |  1 +
 .../MAKE_DIRECTORY-Result-many-dirs-SUCCESS.cmake  |  7 +++
 .../MAKE_DIRECTORY-Result-one-dir-FAIL-stdout.txt  |  3 +
 .../MAKE_DIRECTORY-Result-one-dir-FAIL.cmake       |  3 +
 ...AKE_DIRECTORY-Result-one-dir-SUCCESS-stdout.txt |  1 +
 .../MAKE_DIRECTORY-Result-one-dir-SUCCESS.cmake    |  2 +
 .../MAKE_DIRECTORY-one-dir-FAIL-result.txt         |  1 +
 .../MAKE_DIRECTORY-one-dir-FAIL-stderr.txt         |  9 +++
 .../MAKE_DIRECTORY-one-dir-FAIL.cmake              |  2 +
 .../file-MAKE_DIRECTORY/RunCMakeTest.cmake         |  7 +++
 Tests/RunCMake/file/MAKE_DIRECTORY-fail-result.txt |  1 -
 Tests/RunCMake/file/MAKE_DIRECTORY-fail-stderr.txt |  9 ---
 Tests/RunCMake/file/MAKE_DIRECTORY-fail.cmake      |  2 -
 Tests/RunCMake/file/RunCMakeTest.cmake             |  2 -
 21 files changed, 127 insertions(+), 24 deletions(-)
 create mode 100644 Help/dev/make_directory-optional-result.rst
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/CMakeLists.txt
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL-stdout.txt
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL.cmake
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-SUCCESS-stdout.txt
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-SUCCESS.cmake
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL-stdout.txt
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL.cmake
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-SUCCESS-stdout.txt
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-SUCCESS.cmake
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-result.txt
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-stderr.txt
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL.cmake
 create mode 100644 Tests/RunCMake/file-MAKE_DIRECTORY/RunCMakeTest.cmake
 delete mode 100644 Tests/RunCMake/file/MAKE_DIRECTORY-fail-result.txt
 delete mode 100644 Tests/RunCMake/file/MAKE_DIRECTORY-fail-stderr.txt
 delete mode 100644 Tests/RunCMake/file/MAKE_DIRECTORY-fail.cmake

diff --git a/Help/command/file.rst b/Help/command/file.rst
index 5b9dfac..ede95a1 100644
--- a/Help/command/file.rst
+++ b/Help/command/file.rst
@@ -398,10 +398,19 @@ Filesystem
   ============== ======================================================
 
 .. signature::
-  file(MAKE_DIRECTORY <directories>...)
+  file(MAKE_DIRECTORY <directories>... [RESULT <result>])
 
   Create the given directories and their parents as needed.
 
+  The options are:
+
+    ``RESULT <result>``
+      .. versionadded:: 3.31
+
+      Set ``<result>`` variable to ``0`` on success or an error message
+      otherwise. If ``RESULT`` is not specified and the operation fails,
+      an error is emitted.
+
   .. versionchanged:: 3.30
     ``<directories>`` can be an empty list. CMake 3.29 and earlier required
     at least one directory to be given.
diff --git a/Help/dev/make_directory-optional-result.rst b/Help/dev/make_directory-optional-result.rst
new file mode 100644
index 0000000..be842fe
--- /dev/null
+++ b/Help/dev/make_directory-optional-result.rst
@@ -0,0 +1,5 @@
+make_directory-optional-result
+------------------------------
+
+* The :command:`file(MAKE_DIRECTORY)` learned to
+optionally capture failure in a result variable.
diff --git a/Source/cmFileCommand.cxx b/Source/cmFileCommand.cxx
index ce8cc2a..88555b6 100644
--- a/Source/cmFileCommand.cxx
+++ b/Source/cmFileCommand.cxx
@@ -8,6 +8,7 @@
 #include <cmath>
 #include <cstdio>
 #include <cstdlib>
+#include <iterator>
 #include <map>
 #include <set>
 #include <sstream>
@@ -881,6 +882,42 @@ bool HandleMakeDirectoryCommand(std::vector<std::string> const& args,
   // Projects might pass a dynamically generated list of directories, and it
   // could be an empty list. We should not assume there is at least one.
 
+  cmRange<std::vector<std::string>::const_iterator> argsRange =
+    cmMakeRange(args).advance(1); // Get rid of subcommand
+
+  struct Arguments : public ArgumentParser::ParseResult
+  {
+    std::string Result;
+  };
+  Arguments arguments;
+
+  auto resultPosItr =
+    std::find(cm::begin(argsRange), cm::end(argsRange), "RESULT");
+  if (resultPosItr != cm::end(argsRange)) {
+    static auto const parser =
+      cmArgumentParser<Arguments>{}.Bind("RESULT"_s, &Arguments::Result);
+    std::vector<std::string> unparsedArguments;
+    auto resultDistanceFromBegin =
+      std::distance(cm::begin(argsRange), resultPosItr);
+    arguments =
+      parser.Parse(cmMakeRange(argsRange).advance(resultDistanceFromBegin),
+                   &unparsedArguments);
+
+    if (!unparsedArguments.empty()) {
+      std::string unexpectedArgsStr = cmJoin(
+        cmMakeRange(cm::begin(unparsedArguments), cm::end(unparsedArguments)),
+        "\n");
+      status.SetError("MAKE_DIRECTORY called with unexpected\n"
+                      "arguments:\n" +
+                      unexpectedArgsStr);
+      return false;
+    }
+
+    auto resultDistanceFromEnd =
+      std::distance(cm::end(argsRange), resultPosItr);
+    argsRange = argsRange.retreat(-resultDistanceFromEnd);
+  }
+
   std::string expr;
   for (std::string const& arg :
        cmMakeRange(args).advance(1)) // Get rid of subcommand
@@ -892,20 +929,34 @@ bool HandleMakeDirectoryCommand(std::vector<std::string> const& args,
       cdir = &expr;
     }
     if (!status.GetMakefile().CanIWriteThisFile(*cdir)) {
-      std::string e = "attempted to create a directory: " + *cdir +
-        " into a source directory.";
-      status.SetError(e);
-      cmSystemTools::SetFatalErrorOccurred();
-      return false;
+      std::string e = cmStrCat("attempted to create a directory: ", *cdir,
+                               " into a source directory.");
+      if (arguments.Result.empty()) {
+        status.SetError(e);
+        cmSystemTools::SetFatalErrorOccurred();
+        return false;
+      }
+      status.GetMakefile().AddDefinition(arguments.Result, e);
+      return true;
     }
     cmsys::Status mkdirStatus = cmSystemTools::MakeDirectory(*cdir);
     if (!mkdirStatus) {
-      std::string error = cmStrCat("failed to create directory:\n  ", *cdir,
-                                   "\nbecause: ", mkdirStatus.GetString());
-      status.SetError(error);
-      return false;
+      if (arguments.Result.empty()) {
+        std::string errorOutput =
+          cmStrCat("failed to create directory:\n  ", *cdir,
+                   "\nbecause: ", mkdirStatus.GetString());
+        status.SetError(errorOutput);
+        return false;
+      }
+      std::string errorResult = cmStrCat("Failed to create directory: ", *cdir,
+                                         " Error: ", mkdirStatus.GetString());
+      status.GetMakefile().AddDefinition(arguments.Result, errorResult);
+      return true;
     }
   }
+  if (!arguments.Result.empty()) {
+    status.GetMakefile().AddDefinition(arguments.Result, "0");
+  }
   return true;
 }
 
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index 213c18d..4977359 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -588,6 +588,7 @@ foreach(var
   endif()
 endforeach()
 add_RunCMake_test(file-DOWNLOAD)
+add_RunCMake_test(file-MAKE_DIRECTORY)
 add_RunCMake_test(file-RPATH
   -DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}
   -DCMake_TEST_ELF_LARGE=${CMake_TEST_ELF_LARGE}
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/CMakeLists.txt b/Tests/RunCMake/file-MAKE_DIRECTORY/CMakeLists.txt
new file mode 100644
index 0000000..93ee9df
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/CMakeLists.txt
@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.5)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL-stdout.txt b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL-stdout.txt
new file mode 100644
index 0000000..63d46fd
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL-stdout.txt
@@ -0,0 +1,3 @@
+^-- Result=Failed to create directory: [^
+]*/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL-build/file/directory0 Error: [^
+]*$
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL.cmake b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL.cmake
new file mode 100644
index 0000000..0cfccbf
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-FAIL.cmake
@@ -0,0 +1,9 @@
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/file" "")
+
+file(MAKE_DIRECTORY
+  "${CMAKE_CURRENT_BINARY_DIR}/file/directory0"
+  "${CMAKE_CURRENT_BINARY_DIR}/file/directory1"
+  "${CMAKE_CURRENT_BINARY_DIR}/file/directory2"
+  RESULT resultVal
+)
+message(STATUS "Result=${resultVal}")
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-SUCCESS-stdout.txt b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-SUCCESS-stdout.txt
new file mode 100644
index 0000000..09df4f9
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-SUCCESS-stdout.txt
@@ -0,0 +1 @@
+^-- Result=0
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-SUCCESS.cmake b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-SUCCESS.cmake
new file mode 100644
index 0000000..e0781ce
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-many-dirs-SUCCESS.cmake
@@ -0,0 +1,7 @@
+file(MAKE_DIRECTORY
+  "${CMAKE_CURRENT_BINARY_DIR}/file/directory0"
+  "${CMAKE_CURRENT_BINARY_DIR}/file/directory1"
+  "${CMAKE_CURRENT_BINARY_DIR}/file/directory2"
+  RESULT resultVal
+)
+message(STATUS "Result=${resultVal}")
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL-stdout.txt b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL-stdout.txt
new file mode 100644
index 0000000..5d16178
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL-stdout.txt
@@ -0,0 +1,3 @@
+^-- Result=Failed to create directory: [^
+]*/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL-build/file/directory Error: [^
+]*$
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL.cmake b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL.cmake
new file mode 100644
index 0000000..0287d67
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-FAIL.cmake
@@ -0,0 +1,3 @@
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/file" "")
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/file/directory" RESULT resultVal)
+message(STATUS "Result=${resultVal}")
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-SUCCESS-stdout.txt b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-SUCCESS-stdout.txt
new file mode 100644
index 0000000..09df4f9
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-SUCCESS-stdout.txt
@@ -0,0 +1 @@
+^-- Result=0
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-SUCCESS.cmake b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-SUCCESS.cmake
new file mode 100644
index 0000000..3005b83
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-Result-one-dir-SUCCESS.cmake
@@ -0,0 +1,2 @@
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/file/directory" RESULT resultVal)
+message(STATUS "Result=${resultVal}")
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-result.txt b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-stderr.txt b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-stderr.txt
new file mode 100644
index 0000000..2bc275c
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-stderr.txt
@@ -0,0 +1,9 @@
+^CMake Error at [^
+]*/MAKE_DIRECTORY-one-dir-FAIL.cmake:[0-9]+ \(file\):
+  file failed to create directory:
+
+    [^
+]*/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL-build/file/directory
+
+  because: [^
+]+$
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL.cmake b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL.cmake
new file mode 100644
index 0000000..57a68e5
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/MAKE_DIRECTORY-one-dir-FAIL.cmake
@@ -0,0 +1,2 @@
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/file" "")
+file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/file/directory")
diff --git a/Tests/RunCMake/file-MAKE_DIRECTORY/RunCMakeTest.cmake b/Tests/RunCMake/file-MAKE_DIRECTORY/RunCMakeTest.cmake
new file mode 100644
index 0000000..1eacd90
--- /dev/null
+++ b/Tests/RunCMake/file-MAKE_DIRECTORY/RunCMakeTest.cmake
@@ -0,0 +1,7 @@
+include(RunCMake)
+
+run_cmake_script(MAKE_DIRECTORY-one-dir-FAIL)
+run_cmake_script(MAKE_DIRECTORY-Result-one-dir-FAIL)
+run_cmake_script(MAKE_DIRECTORY-Result-one-dir-SUCCESS)
+run_cmake_script(MAKE_DIRECTORY-Result-many-dirs-FAIL)
+run_cmake_script(MAKE_DIRECTORY-Result-many-dirs-SUCCESS)
diff --git a/Tests/RunCMake/file/MAKE_DIRECTORY-fail-result.txt b/Tests/RunCMake/file/MAKE_DIRECTORY-fail-result.txt
deleted file mode 100644
index d00491f..0000000
--- a/Tests/RunCMake/file/MAKE_DIRECTORY-fail-result.txt
+++ /dev/null
@@ -1 +0,0 @@
-1
diff --git a/Tests/RunCMake/file/MAKE_DIRECTORY-fail-stderr.txt b/Tests/RunCMake/file/MAKE_DIRECTORY-fail-stderr.txt
deleted file mode 100644
index 95fccdf..0000000
--- a/Tests/RunCMake/file/MAKE_DIRECTORY-fail-stderr.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-^CMake Error at [^
-]*/MAKE_DIRECTORY-fail.cmake:[0-9]+ \(file\):
-  file failed to create directory:
-
-    [^
-]*/Tests/RunCMake/file/MAKE_DIRECTORY-fail-build/file/directory
-
-  because: [^
-]+$
diff --git a/Tests/RunCMake/file/MAKE_DIRECTORY-fail.cmake b/Tests/RunCMake/file/MAKE_DIRECTORY-fail.cmake
deleted file mode 100644
index 57a68e5..0000000
--- a/Tests/RunCMake/file/MAKE_DIRECTORY-fail.cmake
+++ /dev/null
@@ -1,2 +0,0 @@
-file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/file" "")
-file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/file/directory")
diff --git a/Tests/RunCMake/file/RunCMakeTest.cmake b/Tests/RunCMake/file/RunCMakeTest.cmake
index be8ee7c..524636d 100644
--- a/Tests/RunCMake/file/RunCMakeTest.cmake
+++ b/Tests/RunCMake/file/RunCMakeTest.cmake
@@ -62,8 +62,6 @@ run_cmake_script(COPY_FILE-arg-unknown)
 run_cmake_script(COPY_FILE-input-missing)
 run_cmake_script(COPY_FILE-output-missing)
 
-run_cmake_script(MAKE_DIRECTORY-fail)
-
 run_cmake_script(RENAME-file-replace)
 run_cmake_script(RENAME-file-to-file)
 run_cmake_script(RENAME-file-to-dir-capture)
-- 
cgit v0.12