VS: Support add_custom_command in .Net SDK-style projects

Fixes: #26048
diff --git a/Source/cmVisualStudio10TargetGenerator.cxx b/Source/cmVisualStudio10TargetGenerator.cxx
index 0fb8bae..212fedb 100644
--- a/Source/cmVisualStudio10TargetGenerator.cxx
+++ b/Source/cmVisualStudio10TargetGenerator.cxx
@@ -914,7 +914,7 @@
     return;
   }
 
-  if (this->HasCustomCommands()) {
+  if (this->HasCustomCommandsSource()) {
     std::string message = cmStrCat(
       "The target \"", this->GeneratorTarget->GetName(),
       "\" does not currently support add_custom_command as the Visual Studio "
@@ -1005,7 +1005,6 @@
     e1.Attribute("Condition",
                  cmStrCat("'$(Configuration)' == '", config, '\''));
     e1.SetHasElements();
-    this->WriteEvents(e1, config);
 
     std::string outDir =
       cmStrCat(this->GeneratorTarget->GetDirectory(config), '/');
@@ -1017,6 +1016,10 @@
     oh.OutputFlagMap();
   }
 
+  for (const std::string& config : this->Configurations) {
+    this->WriteSdkStyleEvents(e0, config);
+  }
+
   this->WriteDotNetDocumentationFile(e0);
   this->WriteAllSources(e0);
   this->WriteEmbeddedResourceGroup(e0);
@@ -1079,14 +1082,8 @@
   }
 }
 
-bool cmVisualStudio10TargetGenerator::HasCustomCommands() const
+bool cmVisualStudio10TargetGenerator::HasCustomCommandsSource() const
 {
-  if (!this->GeneratorTarget->GetPreBuildCommands().empty() ||
-      !this->GeneratorTarget->GetPreLinkCommands().empty() ||
-      !this->GeneratorTarget->GetPostBuildCommands().empty()) {
-    return true;
-  }
-
   auto const& config_sources = this->GeneratorTarget->GetAllConfigSources();
   return std::any_of(config_sources.begin(), config_sources.end(),
                      [](cmGeneratorTarget::AllConfigSource const& si) {
@@ -4871,6 +4868,71 @@
   }
 }
 
+void cmVisualStudio10TargetGenerator::WriteSdkStyleEvents(
+  Elem& e0, std::string const& configName)
+{
+  this->WriteSdkStyleEvent(e0, "PreLink", "BeforeTargets", "Link",
+                           this->GeneratorTarget->GetPreLinkCommands(),
+                           configName);
+  this->WriteSdkStyleEvent(e0, "PreBuild", "BeforeTargets", "PreBuildEvent",
+                           this->GeneratorTarget->GetPreBuildCommands(),
+                           configName);
+  this->WriteSdkStyleEvent(e0, "PostBuild", "AfterTargets", "PostBuildEvent",
+                           this->GeneratorTarget->GetPostBuildCommands(),
+                           configName);
+}
+
+void cmVisualStudio10TargetGenerator::WriteSdkStyleEvent(
+  Elem& e0, const std::string& name, const std::string& when,
+  const std::string& target, std::vector<cmCustomCommand> const& commands,
+  std::string const& configName)
+{
+  if (commands.empty()) {
+    return;
+  }
+  Elem e1(e0, "Target");
+  e1.Attribute("Condition",
+               cmStrCat("'$(Configuration)' == '", configName, '\''));
+  e1.Attribute("Name", name + configName);
+  e1.Attribute(when.c_str(), target);
+  e1.SetHasElements();
+
+  cmLocalVisualStudio7Generator* lg = this->LocalGenerator;
+  std::string script;
+  const char* pre = "";
+  std::string comment;
+  bool stdPipesUTF8 = false;
+  for (cmCustomCommand const& cc : commands) {
+    cmCustomCommandGenerator ccg(cc, configName, lg);
+    if (!ccg.HasOnlyEmptyCommandLines()) {
+      comment += pre;
+      comment += lg->ConstructComment(ccg);
+      script += pre;
+      pre = "\n";
+      script += lg->ConstructScript(ccg);
+
+      stdPipesUTF8 = stdPipesUTF8 || cc.GetStdPipesUTF8();
+    }
+  }
+  if (!script.empty()) {
+    script += lg->FinishConstructScript(this->ProjectType);
+  }
+  comment = cmVS10EscapeComment(comment);
+
+  std::string strippedComment = comment;
+  strippedComment.erase(
+    std::remove(strippedComment.begin(), strippedComment.end(), '\t'),
+    strippedComment.end());
+  std::ostringstream oss;
+  if (!comment.empty() && !strippedComment.empty()) {
+    oss << "echo " << comment << "\n";
+  }
+  oss << script << "\n";
+
+  Elem e2(e1, "Exec");
+  e2.Attribute("Command", oss.str());
+}
+
 void cmVisualStudio10TargetGenerator::WriteProjectReferences(Elem& e0)
 {
   cmGlobalGenerator::TargetDependSet const& unordered =
diff --git a/Source/cmVisualStudio10TargetGenerator.h b/Source/cmVisualStudio10TargetGenerator.h
index 056f426..fae1c89 100644
--- a/Source/cmVisualStudio10TargetGenerator.h
+++ b/Source/cmVisualStudio10TargetGenerator.h
@@ -193,6 +193,11 @@
   void WriteEvent(Elem& e1, std::string const& name,
                   std::vector<cmCustomCommand> const& commands,
                   std::string const& configName);
+  void WriteSdkStyleEvents(Elem& e0, std::string const& configName);
+  void WriteSdkStyleEvent(Elem& e0, const std::string& name,
+                          const std::string& when, const std::string& target,
+                          std::vector<cmCustomCommand> const& commands,
+                          std::string const& configName);
   void WriteGroupSources(Elem& e0, std::string const& name,
                          ToolSources const& sources,
                          std::vector<cmSourceGroup>&);
@@ -282,7 +287,7 @@
   void WriteCommonPropertyGroupGlobals(
     cmVisualStudio10TargetGenerator::Elem& e1);
 
-  bool HasCustomCommands() const;
+  bool HasCustomCommandsSource() const;
 
   std::unordered_map<std::string, ConfigToSettings> ParsedToolTargetSettings;
   bool PropertyIsSameInAllConfigs(const ConfigToSettings& toolSettings,
diff --git a/Tests/RunCMake/VsDotnetSdk/RunCMakeTest.cmake b/Tests/RunCMake/VsDotnetSdk/RunCMakeTest.cmake
index 34259b7..0be77ae 100644
--- a/Tests/RunCMake/VsDotnetSdk/RunCMakeTest.cmake
+++ b/Tests/RunCMake/VsDotnetSdk/RunCMakeTest.cmake
@@ -1,7 +1,6 @@
 cmake_policy(SET CMP0053 NEW)
 include(RunCMake)
 
-run_cmake(VsDotnetSdkCustomCommandsTarget)
 run_cmake(VsDotnetSdkCustomCommandsSource)
 run_cmake(VsDotnetSdkStartupObject)
 run_cmake(VsDotnetSdkDefines)
@@ -18,3 +17,16 @@
   run_cmake_command(VsDotnetSdk-build ${CMAKE_COMMAND} --build . -- ${build_flags})
 endfunction()
 run_VsDotnetSdk()
+
+function(runCmakeAndBuild CASE)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${CASE}-build)
+  set(RunCMake_TEST_NO_CLEAN 1)
+  file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+  run_cmake(${CASE})
+  set(build_flags /restore)
+  run_cmake_command(${CASE}-build ${CMAKE_COMMAND} --build . -- ${build_flags})
+  run_cmake_command(${CASE}-build ${CMAKE_COMMAND} --build .)
+endfunction()
+
+runCmakeAndBuild(VsDotnetSdkCustomCommandsTarget)
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-build-stdout.txt b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-build-stdout.txt
new file mode 100644
index 0000000..890a8f1
--- /dev/null
+++ b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-build-stdout.txt
@@ -0,0 +1 @@
+.*"This should happen!".*
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-result.txt b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-result.txt
deleted file mode 100644
index e69de29..0000000
--- a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-result.txt
+++ /dev/null
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-stderr.txt b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-stderr.txt
deleted file mode 100644
index 90af627..0000000
--- a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-stderr.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-CMake Error in CMakeLists.txt:
-  The target "foo" does not currently support add_custom_command as the
-  Visual Studio generators have not yet learned how to generate custom
-  commands in .Net SDK-style projects.
-
-
-CMake Generate step failed.  Build files cannot be regenerated correctly.
diff --git a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget.cmake b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget.cmake
index f5cd317..078d7dd 100644
--- a/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget.cmake
+++ b/Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget.cmake
@@ -8,5 +8,5 @@
 add_library(foo SHARED lib1.cs)
 add_custom_command(TARGET foo
   PRE_BUILD
-  COMMAND echo "This shouldn't happen!"
+  COMMAND echo "This should happen!"
   VERBATIM)