Browse Source

Merge topic 'vs_csharp_custom_command'

ec409a11 Vs: fix CSharp custom command by introducing inline MSBuild <Targets>s
dcdab5cf Vs: factor out computation of <Link> tag for CSharp source files
0a8f469a Vs: refactor WriteCustomRule for preparation of CSharp support

Acked-by: Kitware Robot <[email protected]>
Merge-request: !970
Brad King 8 years ago
parent
commit
126effbb9c

+ 129 - 30
Source/cmVisualStudio10TargetGenerator.cxx

@@ -27,6 +27,12 @@ static std::string cmVS10EscapeXML(std::string arg)
   return arg;
 }
 
+static std::string cmVS10EscapeQuotes(std::string arg)
+{
+  cmSystemTools::ReplaceString(arg, "\"", "&quot;");
+  return arg;
+}
+
 static std::string cmVS10EscapeComment(std::string comment)
 {
   // MSBuild takes the CDATA of a <Message></Message> element and just
@@ -578,6 +584,18 @@ void cmVisualStudio10TargetGenerator::Generate()
       this->WriteEvents(*i);
       this->WriteString("</PropertyGroup>\n", 1);
     }
+    // make sure custom commands are executed before build (if necessary)
+    this->WriteString("<PropertyGroup>\n", 1);
+    this->WriteString("<BuildDependsOn>\n", 2);
+    for (std::set<std::string>::const_iterator i =
+           this->CSharpCustomCommandNames.begin();
+         i != this->CSharpCustomCommandNames.end(); ++i) {
+      this->WriteString(i->c_str(), 3);
+      (*this->BuildFileStream) << ";\n";
+    }
+    this->WriteString("$(BuildDependsOn)\n", 3);
+    this->WriteString("</BuildDependsOn>\n", 2);
+    this->WriteString("</PropertyGroup>\n", 1);
   }
   this->WriteString("</Project>", 0);
   // The groups are stored in a separate file for VS 10
@@ -1151,6 +1169,7 @@ void cmVisualStudio10TargetGenerator::WriteNsightTegraConfigurationValues(
 void cmVisualStudio10TargetGenerator::WriteCustomCommands()
 {
   this->SourcesVisited.clear();
+  this->CSharpCustomCommandNames.clear();
   std::vector<cmSourceFile const*> customCommands;
   this->GeneratorTarget->GetCustomCommands(customCommands, "");
   for (std::vector<cmSourceFile const*>::const_iterator si =
@@ -1172,9 +1191,14 @@ void cmVisualStudio10TargetGenerator::WriteCustomCommand(
       }
     }
     if (cmCustomCommand const* command = sf->GetCustomCommand()) {
-      this->WriteString("<ItemGroup>\n", 1);
+      // C# projects write their <Target> within WriteCustomRule()
+      if (this->ProjectType != csproj) {
+        this->WriteString("<ItemGroup>\n", 1);
+      }
       this->WriteCustomRule(sf, *command);
-      this->WriteString("</ItemGroup>\n", 1);
+      if (this->ProjectType != csproj) {
+        this->WriteString("</ItemGroup>\n", 1);
+      }
     }
   }
 }
@@ -1209,8 +1233,20 @@ void cmVisualStudio10TargetGenerator::WriteCustomRule(
   }
   cmLocalVisualStudio7Generator* lg = this->LocalGenerator;
 
-  this->WriteSource("CustomBuild", source, ">\n");
-
+  if (this->ProjectType != csproj) {
+    this->WriteSource("CustomBuild", source, ">\n");
+  } else {
+    this->WriteString("<ItemGroup>\n", 1);
+    std::string link;
+    this->GetCSharpSourceLink(source, link);
+    this->WriteSource("None", source, ">\n");
+    if (!link.empty()) {
+      this->WriteString("<Link>", 3);
+      (*this->BuildFileStream) << link << "</Link>\n";
+    }
+    this->WriteString("</None>\n", 2);
+    this->WriteString("</ItemGroup>\n", 1);
+  }
   for (std::vector<std::string>::const_iterator i =
          this->Configurations.begin();
        i != this->Configurations.end(); ++i) {
@@ -1218,41 +1254,91 @@ void cmVisualStudio10TargetGenerator::WriteCustomRule(
     std::string comment = lg->ConstructComment(ccg);
     comment = cmVS10EscapeComment(comment);
     std::string script = cmVS10EscapeXML(lg->ConstructScript(ccg));
-    this->WritePlatformConfigTag("Message", i->c_str(), 3);
-    (*this->BuildFileStream) << cmVS10EscapeXML(comment) << "</Message>\n";
-    this->WritePlatformConfigTag("Command", i->c_str(), 3);
-    (*this->BuildFileStream) << script << "</Command>\n";
-    this->WritePlatformConfigTag("AdditionalInputs", i->c_str(), 3);
-
-    (*this->BuildFileStream) << cmVS10EscapeXML(source->GetFullPath());
+    // input files for custom command
+    std::stringstream inputs;
+    inputs << cmVS10EscapeXML(source->GetFullPath());
     for (std::vector<std::string>::const_iterator d = ccg.GetDepends().begin();
          d != ccg.GetDepends().end(); ++d) {
       std::string dep;
       if (this->LocalGenerator->GetRealDependency(d->c_str(), i->c_str(),
                                                   dep)) {
         this->ConvertToWindowsSlash(dep);
-        (*this->BuildFileStream) << ";" << cmVS10EscapeXML(dep);
+        inputs << ";" << cmVS10EscapeXML(dep);
       }
     }
-    (*this->BuildFileStream) << ";%(AdditionalInputs)</AdditionalInputs>\n";
-    this->WritePlatformConfigTag("Outputs", i->c_str(), 3);
+    // output files for custom command
+    std::stringstream outputs;
     const char* sep = "";
     for (std::vector<std::string>::const_iterator o = ccg.GetOutputs().begin();
          o != ccg.GetOutputs().end(); ++o) {
       std::string out = *o;
       this->ConvertToWindowsSlash(out);
-      (*this->BuildFileStream) << sep << cmVS10EscapeXML(out);
+      outputs << sep << cmVS10EscapeXML(out);
       sep = ";";
     }
-    (*this->BuildFileStream) << "</Outputs>\n";
-    if (this->LocalGenerator->GetVersion() >
-        cmGlobalVisualStudioGenerator::VS10) {
-      // VS >= 11 let us turn off linking of custom command outputs.
-      this->WritePlatformConfigTag("LinkObjects", i->c_str(), 3);
-      (*this->BuildFileStream) << "false</LinkObjects>\n";
+    if (this->ProjectType == csproj) {
+      std::string name = "CustomCommand_" + *i + "_" +
+        cmSystemTools::ComputeStringMD5(sourcePath);
+      std::string inputs_s = inputs.str();
+      std::string outputs_s = outputs.str();
+      comment = cmVS10EscapeQuotes(comment);
+      script = cmVS10EscapeQuotes(script);
+      inputs_s = cmVS10EscapeQuotes(inputs_s);
+      outputs_s = cmVS10EscapeQuotes(outputs_s);
+      this->WriteCustomRuleCSharp(*i, name, script, inputs_s, outputs_s,
+                                  comment);
+    } else {
+      this->WriteCustomRuleCpp(*i, script, inputs.str(), outputs.str(),
+                               comment);
     }
   }
-  this->WriteString("</CustomBuild>\n", 2);
+  if (this->ProjectType != csproj) {
+    this->WriteString("</CustomBuild>\n", 2);
+  }
+}
+
+void cmVisualStudio10TargetGenerator::WriteCustomRuleCpp(
+  std::string const& config, std::string const& script,
+  std::string const& inputs, std::string const& outputs,
+  std::string const& comment)
+{
+  this->WritePlatformConfigTag("Message", config, 3);
+  (*this->BuildFileStream) << cmVS10EscapeXML(comment) << "</Message>\n";
+  this->WritePlatformConfigTag("Command", config, 3);
+  (*this->BuildFileStream) << script << "</Command>\n";
+  this->WritePlatformConfigTag("AdditionalInputs", config, 3);
+  (*this->BuildFileStream) << inputs;
+  (*this->BuildFileStream) << ";%(AdditionalInputs)</AdditionalInputs>\n";
+  this->WritePlatformConfigTag("Outputs", config, 3);
+  (*this->BuildFileStream) << outputs << "</Outputs>\n";
+  if (this->LocalGenerator->GetVersion() >
+      cmGlobalVisualStudioGenerator::VS10) {
+    // VS >= 11 let us turn off linking of custom command outputs.
+    this->WritePlatformConfigTag("LinkObjects", config, 3);
+    (*this->BuildFileStream) << "false</LinkObjects>\n";
+  }
+}
+
+void cmVisualStudio10TargetGenerator::WriteCustomRuleCSharp(
+  std::string const& config, std::string const& name,
+  std::string const& script, std::string const& inputs,
+  std::string const& outputs, std::string const& comment)
+{
+  this->CSharpCustomCommandNames.insert(name);
+  std::stringstream attributes;
+  attributes << "\n    Name=\"" << name << "\"";
+  attributes << "\n    Inputs=\"" << inputs << "\"";
+  attributes << "\n    Outputs=\"" << outputs << "\"";
+  this->WritePlatformConfigTag("Target", config, 1, attributes.str().c_str(),
+                               "\n");
+  if (!comment.empty()) {
+    this->WriteString("<Exec Command=\"", 2);
+    (*this->BuildFileStream) << "echo " << cmVS10EscapeXML(comment)
+                             << "\" />\n";
+  }
+  this->WriteString("<Exec Command=\"", 2);
+  (*this->BuildFileStream) << script << "\" />\n";
+  this->WriteString("</Target>\n", 1);
 }
 
 std::string cmVisualStudio10TargetGenerator::ConvertPath(
@@ -2044,14 +2130,10 @@ bool cmVisualStudio10TargetGenerator::OutputSourceSpecificFlags(
     typedef std::map<std::string, std::string> CsPropMap;
     CsPropMap sourceFileTags;
     // set <Link> tag if necessary
-    if (!this->InSourceBuild) {
-      const std::string stripFromPath =
-        this->Makefile->GetCurrentSourceDirectory();
-      if (f.find(stripFromPath) != std::string::npos) {
-        std::string link = f.substr(stripFromPath.length() + 1);
-        this->ConvertToWindowsSlash(link);
-        sourceFileTags["Link"] = link;
-      }
+    std::string link;
+    this->GetCSharpSourceLink(source, link);
+    if (!link.empty()) {
+      sourceFileTags["Link"] = link;
     }
     this->GetCSharpSourceProperties(&sf, sourceFileTags);
     // write source file specific tags
@@ -4368,6 +4450,23 @@ void cmVisualStudio10TargetGenerator::WriteCSharpSourceProperties(
   }
 }
 
+void cmVisualStudio10TargetGenerator::GetCSharpSourceLink(
+  cmSourceFile const* sf, std::string& link)
+{
+  std::string f = sf->GetFullPath();
+  if (!this->InSourceBuild) {
+    const std::string stripFromPath =
+      this->Makefile->GetCurrentSourceDirectory();
+    if (f.find(stripFromPath) != std::string::npos) {
+      link = f.substr(stripFromPath.length() + 1);
+      if (const char* l = sf->GetProperty("VS_CSHARP_Link")) {
+        link = l;
+      }
+      this->ConvertToWindowsSlash(link);
+    }
+  }
+}
+
 std::string cmVisualStudio10TargetGenerator::GetCMakeFilePath(
   const char* relativeFilePath) const
 {

+ 12 - 0
Source/cmVisualStudio10TargetGenerator.h

@@ -127,6 +127,16 @@ private:
   void OutputLinkIncremental(std::string const& configName);
   void WriteCustomRule(cmSourceFile const* source,
                        cmCustomCommand const& command);
+  void WriteCustomRuleCpp(std::string const& config, std::string const& script,
+                          std::string const& inputs,
+                          std::string const& outputs,
+                          std::string const& comment);
+  void WriteCustomRuleCSharp(std::string const& config,
+                             std::string const& commandName,
+                             std::string const& script,
+                             std::string const& inputs,
+                             std::string const& outputs,
+                             std::string const& comment);
   void WriteCustomCommands();
   void WriteCustomCommand(cmSourceFile const* sf);
   void WriteGroups();
@@ -158,6 +168,7 @@ private:
                                  std::map<std::string, std::string>& tags);
   void WriteCSharpSourceProperties(
     const std::map<std::string, std::string>& tags);
+  void GetCSharpSourceLink(cmSourceFile const* sf, std::string& link);
 
 private:
   typedef cmVisualStudioGeneratorOptions Options;
@@ -193,6 +204,7 @@ private:
   cmGeneratedFileStream* BuildFileStream;
   cmLocalVisualStudio7Generator* LocalGenerator;
   std::set<cmSourceFile const*> SourcesVisited;
+  std::set<std::string> CSharpCustomCommandNames;
   bool IsMissingFiles;
   std::vector<std::string> AddedFiles;
   std::string DefaultArtifactDir;

+ 4 - 0
Tests/RunCMake/CMakeLists.txt

@@ -376,3 +376,7 @@ if(CMake_TEST_ANDROID_NDK OR CMake_TEST_ANDROID_STANDALONE_TOOLCHAIN)
   endif()
   set_property(TEST RunCMake.Android PROPERTY TIMEOUT ${CMake_TEST_ANDROID_TIMEOUT})
 endif()
+
+if(${CMAKE_GENERATOR} MATCHES "Visual Studio ([^89]|[89][0-9])")
+  add_RunCMake_test(CSharpCustomCommand)
+endif()

+ 3 - 0
Tests/RunCMake/CSharpCustomCommand/CMakeLists.txt

@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.3)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)

+ 21 - 0
Tests/RunCMake/CSharpCustomCommand/CommandWithOutput-check.cmake

@@ -0,0 +1,21 @@
+if(checkLevel EQUAL 0)
+  message("checking generation (${srcName} does not exist)")
+  if(EXISTS "${generatedFileName}")
+    set(RunCMake_TEST_FAILED "file \"${generatedFileName}\" should not exist")
+  endif()
+elseif(checkLevel EQUAL 1)
+  message("checking build 1 (generate ${srcName})")
+  if(NOT "${actual_stdout}" MATCHES "${commandComment}")
+    set(RunCMake_TEST_FAILED "command not executed")
+  endif()
+elseif(checkLevel EQUAL 2)
+  message("checking build 2 (no change in ${srcName}.in)")
+  if("${actual_stdout}" MATCHES "${commandComment}")
+    set(RunCMake_TEST_FAILED "command executed")
+  endif()
+elseif(checkLevel EQUAL 3)
+  message("checking build 3 (update ${srcName})")
+  if(NOT "${actual_stdout}" MATCHES "${commandComment}")
+    set(RunCMake_TEST_FAILED "command not executed")
+  endif()
+endif()

+ 13 - 0
Tests/RunCMake/CSharpCustomCommand/CommandWithOutput.cmake

@@ -0,0 +1,13 @@
+enable_language(CSharp)
+
+add_executable(CSharpCustomCommand dummy.cs)
+
+add_custom_command(OUTPUT ${generatedFileName}
+  COMMAND ${CMAKE_COMMAND} -E copy_if_different
+    ${inputFileName} ${generatedFileName}
+  MAIN_DEPENDENCY ${inputFileName}
+  COMMENT "${commandComment}")
+
+target_sources(CSharpCustomCommand PRIVATE
+  ${inputFileName}
+  ${generatedFileName})

+ 34 - 0
Tests/RunCMake/CSharpCustomCommand/RunCMakeTest.cmake

@@ -0,0 +1,34 @@
+include(RunCMake)
+
+# Use a single build tree for a few tests without cleaning.
+set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/CommandWithOutput-build)
+set(RunCMake_TEST_NO_CLEAN 1)
+file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+set(RunCMake-check-file CommandWithOutput-check.cmake)
+
+set(srcName "test.cs")
+set(srcFileName "${CMAKE_CURRENT_LIST_DIR}/${srcName}.in")
+set(inputFileName "${RunCMake_TEST_BINARY_DIR}/${srcName}.in")
+set(generatedFileName "${RunCMake_TEST_BINARY_DIR}/${srcName}")
+set(commandComment "Generating ${srcName}")
+
+# copy the input file to build dir to avoid changing files in cmake
+# source tree.
+file(COPY "${srcFileName}" DESTINATION "${RunCMake_TEST_BINARY_DIR}")
+
+set(RunCMake_TEST_OPTIONS ${RunCMake_TEST_OPTIONS}
+  "-DinputFileName=${inputFileName}"
+  "-DgeneratedFileName=${generatedFileName}"
+  "-DcommandComment=${commandComment}")
+
+set(checkLevel 0)
+run_cmake(CommandWithOutput)
+set(checkLevel 1)
+run_cmake_command(CommandWithOutput-build1 ${CMAKE_COMMAND} --build . --config Debug)
+set(checkLevel 2)
+run_cmake_command(CommandWithOutput-build2 ${CMAKE_COMMAND} --build . --config Debug)
+# change file content to trigger custom command with next build
+file(APPEND ${inputFileName} "\n")
+set(checkLevel 3)
+run_cmake_command(CommandWithOutput-build3 ${CMAKE_COMMAND} --build . --config Debug)

+ 0 - 0
Tests/RunCMake/CSharpCustomCommand/dummy.cs


+ 8 - 0
Tests/RunCMake/CSharpCustomCommand/test.cs.in

@@ -0,0 +1,8 @@
+class TestCs
+{
+  public static int Main(string[] args)
+  {
+    System.Console.WriteLine("Test C#");
+    return 0;
+  }
+}