Browse Source

Merge topic 'vs-csharp-dotnet-sdk'

0eea32a376 VS: Add DOTNET_SDK property to generate SDK-style C# projects
a450cc9533 VS: Set ResolveNugetPackages to false for ALL_BUILD and ZERO_CHECK
fa76e5d194 cmVisualStudio10TargetGenerator: Factor out helper for classic MSBuild project

Acked-by: Kitware Robot <[email protected]>
Merge-request: !6634
Brad King 3 years ago
parent
commit
138aabfa9d

+ 2 - 0
Auxiliary/vim/syntax/cmake.vim

@@ -152,6 +152,7 @@ syn keyword cmakeProperty contained
             \ DISABLED
             \ DISABLED
             \ DISABLED_FEATURES
             \ DISABLED_FEATURES
             \ DISABLE_PRECOMPILE_HEADERS
             \ DISABLE_PRECOMPILE_HEADERS
+            \ DOTNET_SDK
             \ DOTNET_TARGET_FRAMEWORK
             \ DOTNET_TARGET_FRAMEWORK
             \ DOTNET_TARGET_FRAMEWORK_VERSION
             \ DOTNET_TARGET_FRAMEWORK_VERSION
             \ ECLIPSE_EXTRA_CPROJECT_CONTENTS
             \ ECLIPSE_EXTRA_CPROJECT_CONTENTS
@@ -1001,6 +1002,7 @@ syn keyword cmakeVariable contained
             \ CMAKE_DIRECTORY_LABELS
             \ CMAKE_DIRECTORY_LABELS
             \ CMAKE_DISABLE_PRECOMPILE_HEADERS
             \ CMAKE_DISABLE_PRECOMPILE_HEADERS
             \ CMAKE_DL_LIBS
             \ CMAKE_DL_LIBS
+            \ CMAKE_DOTNET_SDK
             \ CMAKE_DOTNET_TARGET_FRAMEWORK
             \ CMAKE_DOTNET_TARGET_FRAMEWORK
             \ CMAKE_DOTNET_TARGET_FRAMEWORK_VERSION
             \ CMAKE_DOTNET_TARGET_FRAMEWORK_VERSION
             \ CMAKE_ECLIPSE_GENERATE_LINKED_RESOURCES
             \ CMAKE_ECLIPSE_GENERATE_LINKED_RESOURCES

+ 1 - 0
Help/manual/cmake-properties.7.rst

@@ -191,6 +191,7 @@ Properties on Targets
    /prop_tgt/DEPLOYMENT_REMOTE_DIRECTORY
    /prop_tgt/DEPLOYMENT_REMOTE_DIRECTORY
    /prop_tgt/DEPRECATION
    /prop_tgt/DEPRECATION
    /prop_tgt/DISABLE_PRECOMPILE_HEADERS
    /prop_tgt/DISABLE_PRECOMPILE_HEADERS
+   /prop_tgt/DOTNET_SDK
    /prop_tgt/DOTNET_TARGET_FRAMEWORK
    /prop_tgt/DOTNET_TARGET_FRAMEWORK
    /prop_tgt/DOTNET_TARGET_FRAMEWORK_VERSION
    /prop_tgt/DOTNET_TARGET_FRAMEWORK_VERSION
    /prop_tgt/EchoString
    /prop_tgt/EchoString

+ 1 - 0
Help/manual/cmake-variables.7.rst

@@ -49,6 +49,7 @@ Variables that Provide Information
    /variable/CMAKE_DEBUG_TARGET_PROPERTIES
    /variable/CMAKE_DEBUG_TARGET_PROPERTIES
    /variable/CMAKE_DIRECTORY_LABELS
    /variable/CMAKE_DIRECTORY_LABELS
    /variable/CMAKE_DL_LIBS
    /variable/CMAKE_DL_LIBS
+   /variable/CMAKE_DOTNET_SDK
    /variable/CMAKE_DOTNET_TARGET_FRAMEWORK
    /variable/CMAKE_DOTNET_TARGET_FRAMEWORK
    /variable/CMAKE_DOTNET_TARGET_FRAMEWORK_VERSION
    /variable/CMAKE_DOTNET_TARGET_FRAMEWORK_VERSION
    /variable/CMAKE_EDIT_COMMAND
    /variable/CMAKE_EDIT_COMMAND

+ 25 - 0
Help/prop_tgt/DOTNET_SDK.rst

@@ -0,0 +1,25 @@
+DOTNET_SDK
+----------
+
+.. versionadded:: 3.23
+
+Specify the .NET SDK for C# projects.  For example: ``Microsoft.NET.Sdk``.
+
+This property tells :ref:`Visual Studio Generators` for VS 2019 and
+above to generate a .NET SDK-style project using the specified SDK.
+The property is meaningful only to these generators, and only in C#
+targets.  It is ignored for C++ projects, even if they are managed
+(e.g. using :prop_tgt:`COMMON_LANGUAGE_RUNTIME`).
+
+This property must be a non-empty string to generate .NET SDK-style projects.
+CMake does not perform any validations for the value of the property.
+
+This property may be initialized for all targets using the
+:variable:`CMAKE_DOTNET_SDK` variable.
+
+.. note::
+
+  The :ref:`Visual Studio Generators` in this version of CMake have not
+  yet learned to support :command:`add_custom_command` in .NET SDK-style
+  projects.  It is currently an error to attach a custom command to a
+  target with the ``DOTNET_SDK`` property set.

+ 9 - 0
Help/release/dev/vs-csharp-dotnet-sdk.rst

@@ -0,0 +1,9 @@
+vs-csharp-dotnet-sdk
+--------------------
+
+* The :ref:`Visual Studio Generators` for VS 2019 and above learned to
+  support .NET SDK-style project files (``.csproj``) for C# projects.
+  See the :prop_tgt:`DOTNET_SDK` target property and corresponding
+  :variable:`CMAKE_DOTNET_SDK` variable.
+  However, this version of CMake does not yet support using
+  :command:`add_custom_command` in .NET SDK-style projects.

+ 9 - 0
Help/variable/CMAKE_DOTNET_SDK.rst

@@ -0,0 +1,9 @@
+CMAKE_DOTNET_SDK
+----------------
+
+.. versionadded:: 3.23
+
+Default value for :prop_tgt:`DOTNET_SDK` property of targets.
+
+This variable is used to initialize the :prop_tgt:`DOTNET_SDK`
+property on all targets. See that target property for additional information.

+ 5 - 0
Source/cmGeneratorTarget.cxx

@@ -7842,6 +7842,11 @@ bool cmGeneratorTarget::IsCSharpOnly() const
   return languages.size() == 1 && languages.count("CSharp") > 0;
   return languages.size() == 1 && languages.count("CSharp") > 0;
 }
 }
 
 
+bool cmGeneratorTarget::IsDotNetSdkTarget() const
+{
+  return !this->GetProperty("DOTNET_SDK").IsEmpty();
+}
+
 void cmGeneratorTarget::ComputeLinkImplementationLanguages(
 void cmGeneratorTarget::ComputeLinkImplementationLanguages(
   const std::string& config, cmOptionalLinkImplementation& impl) const
   const std::string& config, cmOptionalLinkImplementation& impl) const
 {
 {

+ 2 - 0
Source/cmGeneratorTarget.h

@@ -436,6 +436,8 @@ public:
 
 
   bool IsCSharpOnly() const;
   bool IsCSharpOnly() const;
 
 
+  bool IsDotNetSdkTarget() const;
+
   void GetObjectLibrariesCMP0026(
   void GetObjectLibrariesCMP0026(
     std::vector<cmGeneratorTarget*>& objlibs) const;
     std::vector<cmGeneratorTarget*>& objlibs) const;
 
 

+ 9 - 1
Source/cmGlobalVisualStudio7Generator.cxx

@@ -373,8 +373,16 @@ void cmGlobalVisualStudio7Generator::WriteTargetConfigurations(
         this->IsPartOfDefaultBuild(configs, projectTargets, target);
         this->IsPartOfDefaultBuild(configs, projectTargets, target);
       cmValue vcprojName = target->GetProperty("GENERATOR_FILE_NAME");
       cmValue vcprojName = target->GetProperty("GENERATOR_FILE_NAME");
       if (vcprojName) {
       if (vcprojName) {
+        std::string mapping;
+
+        // On VS 19 and above, always map .NET SDK projects to "Any CPU".
+        if (target->IsDotNetSdkTarget() &&
+            this->GetVersion() >= VSVersion::VS16 &&
+            !this->IsReservedTarget(target->GetName())) {
+          mapping = "Any CPU";
+        }
         this->WriteProjectConfigurations(fout, *vcprojName, *target, configs,
         this->WriteProjectConfigurations(fout, *vcprojName, *target, configs,
-                                         configsPartOfDefaultBuild);
+                                         configsPartOfDefaultBuild, mapping);
       }
       }
     }
     }
   }
   }

+ 4 - 0
Source/cmTarget.cxx

@@ -529,6 +529,10 @@ cmTarget::cmTarget(std::string const& name, cmStateEnums::TargetType type,
     this->impl->PolicyMap.Set(cmPolicies::CMP0022, cmPolicies::NEW);
     this->impl->PolicyMap.Set(cmPolicies::CMP0022, cmPolicies::NEW);
   }
   }
 
 
+  if (!this->IsImported()) {
+    initProp("DOTNET_SDK");
+  }
+
   if (this->impl->TargetType <= cmStateEnums::GLOBAL_TARGET) {
   if (this->impl->TargetType <= cmStateEnums::GLOBAL_TARGET) {
     initProp("DOTNET_TARGET_FRAMEWORK");
     initProp("DOTNET_TARGET_FRAMEWORK");
     initProp("DOTNET_TARGET_FRAMEWORK_VERSION");
     initProp("DOTNET_TARGET_FRAMEWORK_VERSION");

+ 192 - 52
Source/cmVisualStudio10TargetGenerator.cxx

@@ -405,6 +405,27 @@ void cmVisualStudio10TargetGenerator::Generate()
   // Write the encoding header into the file
   // Write the encoding header into the file
   char magic[] = { char(0xEF), char(0xBB), char(0xBF) };
   char magic[] = { char(0xEF), char(0xBB), char(0xBF) };
   BuildFileStream.write(magic, 3);
   BuildFileStream.write(magic, 3);
+
+  if (this->Managed && this->ProjectType == VsProjectType::csproj &&
+      this->GeneratorTarget->IsDotNetSdkTarget() &&
+      this->GlobalGenerator->GetVersion() >=
+        cmGlobalVisualStudioGenerator::VS16) {
+    this->WriteSdkStyleProjectFile(BuildFileStream);
+  } else {
+    this->WriteClassicMsBuildProjectFile(BuildFileStream);
+  }
+
+  if (BuildFileStream.Close()) {
+    this->GlobalGenerator->FileReplacedDuringGenerate(PathToProjectFile);
+  }
+
+  // The groups are stored in a separate file for VS 10
+  this->WriteGroups();
+}
+
+void cmVisualStudio10TargetGenerator::WriteClassicMsBuildProjectFile(
+  cmGeneratedFileStream& BuildFileStream)
+{
   BuildFileStream << "<?xml version=\"1.0\" encoding=\""
   BuildFileStream << "<?xml version=\"1.0\" encoding=\""
                   << this->GlobalGenerator->Encoding() << "\"?>";
                   << this->GlobalGenerator->Encoding() << "\"?>";
   {
   {
@@ -447,14 +468,27 @@ void cmVisualStudio10TargetGenerator::Generate()
       e1.Element("PreferredToolArchitecture", hostArch);
       e1.Element("PreferredToolArchitecture", hostArch);
     }
     }
 
 
+    // ALL_BUILD and ZERO_CHECK projects transitively include
+    // Microsoft.Common.CurrentVersion.targets which triggers Target
+    // ResolveNugetPackageAssets when SDK-style targets are in the project.
+    // However, these projects have no nuget packages to reference and the
+    // build fails.
+    // Setting ResolveNugetPackages to false skips this target and the build
+    // succeeds.
+    cm::string_view targetName{ this->GeneratorTarget->GetName() };
+    if (targetName == "ALL_BUILD" ||
+        targetName == CMAKE_CHECK_BUILD_SYSTEM_TARGET) {
+      Elem e1(e0, "PropertyGroup");
+      e1.Element("ResolveNugetPackages", "false");
+    }
+
     if (this->ProjectType != VsProjectType::csproj) {
     if (this->ProjectType != VsProjectType::csproj) {
       this->WriteProjectConfigurations(e0);
       this->WriteProjectConfigurations(e0);
     }
     }
 
 
     {
     {
       Elem e1(e0, "PropertyGroup");
       Elem e1(e0, "PropertyGroup");
-      e1.Attribute("Label", "Globals");
-      e1.Element("ProjectGuid", "{" + this->GUID + "}");
+      this->WriteCommonPropertyGroupGlobals(e1);
 
 
       if ((this->MSTools || this->Android) &&
       if ((this->MSTools || this->Android) &&
           this->GeneratorTarget->IsInBuildSystem()) {
           this->GeneratorTarget->IsInBuildSystem()) {
@@ -462,16 +496,6 @@ void cmVisualStudio10TargetGenerator::Generate()
         this->VerifyNecessaryFiles();
         this->VerifyNecessaryFiles();
       }
       }
 
 
-      cmValue vsProjectTypes =
-        this->GeneratorTarget->GetProperty("VS_GLOBAL_PROJECT_TYPES");
-      if (vsProjectTypes) {
-        const char* tagName = "ProjectTypes";
-        if (this->ProjectType == VsProjectType::csproj) {
-          tagName = "ProjectTypeGuids";
-        }
-        e1.Element(tagName, *vsProjectTypes);
-      }
-
       cmValue vsProjectName =
       cmValue vsProjectName =
         this->GeneratorTarget->GetProperty("VS_SCC_PROJECTNAME");
         this->GeneratorTarget->GetProperty("VS_SCC_PROJECTNAME");
       cmValue vsLocalPath =
       cmValue vsLocalPath =
@@ -495,24 +519,6 @@ void cmVisualStudio10TargetGenerator::Generate()
         e1.Element("WinMDAssembly", "true");
         e1.Element("WinMDAssembly", "true");
       }
       }
 
 
-      cmValue vsGlobalKeyword =
-        this->GeneratorTarget->GetProperty("VS_GLOBAL_KEYWORD");
-      if (!vsGlobalKeyword) {
-        if (this->GlobalGenerator->TargetsAndroid()) {
-          e1.Element("Keyword", "Android");
-        } else {
-          e1.Element("Keyword", "Win32Proj");
-        }
-      } else {
-        e1.Element("Keyword", *vsGlobalKeyword);
-      }
-
-      cmValue vsGlobalRootNamespace =
-        this->GeneratorTarget->GetProperty("VS_GLOBAL_ROOTNAMESPACE");
-      if (vsGlobalRootNamespace) {
-        e1.Element("RootNamespace", *vsGlobalRootNamespace);
-      }
-
       e1.Element("Platform", this->Platform);
       e1.Element("Platform", this->Platform);
       cmValue projLabel = this->GeneratorTarget->GetProperty("PROJECT_LABEL");
       cmValue projLabel = this->GeneratorTarget->GetProperty("PROJECT_LABEL");
       e1.Element("ProjectName", projLabel ? projLabel : this->Name);
       e1.Element("ProjectName", projLabel ? projLabel : this->Name);
@@ -602,24 +608,6 @@ void cmVisualStudio10TargetGenerator::Generate()
         e1.Element("VCTargetsPath", vcTargetsPath);
         e1.Element("VCTargetsPath", vcTargetsPath);
       }
       }
 
 
-      std::vector<std::string> keys = this->GeneratorTarget->GetPropertyKeys();
-      for (std::string const& keyIt : keys) {
-        static const cm::string_view prefix = "VS_GLOBAL_";
-        if (!cmHasPrefix(keyIt, prefix))
-          continue;
-        cm::string_view globalKey =
-          cm::string_view(keyIt).substr(prefix.length());
-        // Skip invalid or separately-handled properties.
-        if (globalKey.empty() || globalKey == "PROJECT_TYPES" ||
-            globalKey == "ROOTNAMESPACE" || globalKey == "KEYWORD") {
-          continue;
-        }
-        cmValue value = this->GeneratorTarget->GetProperty(keyIt);
-        if (!value)
-          continue;
-        e1.Element(globalKey, *value);
-      }
-
       if (this->Managed) {
       if (this->Managed) {
         if (this->LocalGenerator->GetVersion() >=
         if (this->LocalGenerator->GetVersion() >=
             cmGlobalVisualStudioGenerator::VS17) {
             cmGlobalVisualStudioGenerator::VS17) {
@@ -839,13 +827,165 @@ void cmVisualStudio10TargetGenerator::Generate()
       }
       }
     }
     }
   }
   }
+}
 
 
-  if (BuildFileStream.Close()) {
-    this->GlobalGenerator->FileReplacedDuringGenerate(PathToProjectFile);
+void cmVisualStudio10TargetGenerator::WriteSdkStyleProjectFile(
+  cmGeneratedFileStream& BuildFileStream)
+{
+  if (!this->Managed || this->ProjectType != VsProjectType::csproj ||
+      !this->GeneratorTarget->IsDotNetSdkTarget()) {
+    std::string message = "The target \"" + this->GeneratorTarget->GetName() +
+      "\" is not eligible for .Net SDK style project.";
+    this->Makefile->IssueMessage(MessageType::INTERNAL_ERROR, message);
+    return;
   }
   }
 
 
-  // The groups are stored in a separate file for VS 10
-  this->WriteGroups();
+  if (this->HasCustomCommands()) {
+    std::string message = "The target \"" + this->GeneratorTarget->GetName() +
+      "\" 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.";
+    this->Makefile->IssueMessage(MessageType::FATAL_ERROR, message);
+    return;
+  }
+
+  Elem e0(BuildFileStream, "Project");
+  e0.Attribute("Sdk", *this->GeneratorTarget->GetProperty("DOTNET_SDK"));
+
+  {
+    Elem e1(e0, "PropertyGroup");
+    this->WriteCommonPropertyGroupGlobals(e1);
+
+    e1.Element("EnableDefaultItems", "false");
+    // Disable the project upgrade prompt that is displayed the first time a
+    // project using an older toolset version is opened in a newer version
+    // of the IDE.
+    e1.Element("VCProjectUpgraderObjectName", "NoUpgrade");
+    e1.Element("ManagedAssembly", "true");
+
+    cmValue targetFramework =
+      this->GeneratorTarget->GetProperty("DOTNET_TARGET_FRAMEWORK");
+    if (targetFramework) {
+      if (targetFramework->find(';') != std::string::npos) {
+        e1.Element("TargetFrameworks", *targetFramework);
+      } else {
+        e1.Element("TargetFramework", *targetFramework);
+      }
+    } else {
+      e1.Element("TargetFramework", "net5.0");
+    }
+
+    std::string outputType;
+    switch (this->GeneratorTarget->GetType()) {
+      case cmStateEnums::OBJECT_LIBRARY:
+      case cmStateEnums::STATIC_LIBRARY:
+      case cmStateEnums::MODULE_LIBRARY:
+        this->Makefile->IssueMessage(
+          MessageType::FATAL_ERROR,
+          cmStrCat("Target \"", this->GeneratorTarget->GetName(),
+                   "\" is of a type not supported for managed binaries."));
+        return;
+      case cmStateEnums::SHARED_LIBRARY:
+        outputType = "Library";
+        break;
+      case cmStateEnums::EXECUTABLE: {
+        auto const win32 =
+          this->GeneratorTarget->GetSafeProperty("WIN32_EXECUTABLE");
+        if (win32.find("$<") != std::string::npos) {
+          this->Makefile->IssueMessage(
+            MessageType::FATAL_ERROR,
+            cmStrCat("Target \"", this->GeneratorTarget->GetName(),
+                     "\" has a generator expression in its WIN32_EXECUTABLE "
+                     "property. This is not supported on managed "
+                     "executables."));
+          return;
+        }
+        outputType = "Exe";
+      } break;
+      case cmStateEnums::UTILITY:
+      case cmStateEnums::INTERFACE_LIBRARY:
+      case cmStateEnums::GLOBAL_TARGET:
+        outputType = "Utility";
+        break;
+      case cmStateEnums::UNKNOWN_LIBRARY:
+        break;
+    }
+    e1.Element("OutputType", outputType);
+  }
+
+  this->WriteDotNetDocumentationFile(e0);
+  this->WriteAllSources(e0);
+  this->WritePackageReferences(e0);
+  this->WriteProjectReferences(e0);
+}
+
+void cmVisualStudio10TargetGenerator::WriteCommonPropertyGroupGlobals(Elem& e1)
+{
+  e1.Attribute("Label", "Globals");
+  e1.Element("ProjectGuid", "{" + this->GUID + "}");
+
+  cmValue vsProjectTypes =
+    this->GeneratorTarget->GetProperty("VS_GLOBAL_PROJECT_TYPES");
+  if (vsProjectTypes) {
+    const char* tagName = "ProjectTypes";
+    if (this->ProjectType == VsProjectType::csproj) {
+      tagName = "ProjectTypeGuids";
+    }
+    e1.Element(tagName, *vsProjectTypes);
+  }
+
+  cmValue vsGlobalKeyword =
+    this->GeneratorTarget->GetProperty("VS_GLOBAL_KEYWORD");
+  if (!vsGlobalKeyword) {
+    if (this->GlobalGenerator->TargetsAndroid()) {
+      e1.Element("Keyword", "Android");
+    } else {
+      e1.Element("Keyword", "Win32Proj");
+    }
+  } else {
+    e1.Element("Keyword", *vsGlobalKeyword);
+  }
+
+  cmValue vsGlobalRootNamespace =
+    this->GeneratorTarget->GetProperty("VS_GLOBAL_ROOTNAMESPACE");
+  if (vsGlobalRootNamespace) {
+    e1.Element("RootNamespace", *vsGlobalRootNamespace);
+  }
+
+  std::vector<std::string> keys = this->GeneratorTarget->GetPropertyKeys();
+  for (std::string const& keyIt : keys) {
+    static const cm::string_view prefix = "VS_GLOBAL_";
+    if (!cmHasPrefix(keyIt, prefix))
+      continue;
+    cm::string_view globalKey = cm::string_view(keyIt).substr(prefix.length());
+    // Skip invalid or separately-handled properties.
+    if (globalKey.empty() || globalKey == "PROJECT_TYPES" ||
+        globalKey == "ROOTNAMESPACE" || globalKey == "KEYWORD") {
+      continue;
+    }
+    cmValue value = this->GeneratorTarget->GetProperty(keyIt);
+    if (!value)
+      continue;
+    e1.Element(globalKey, *value);
+  }
+}
+
+bool cmVisualStudio10TargetGenerator::HasCustomCommands() const
+{
+  if (!this->GeneratorTarget->GetPreBuildCommands().empty() ||
+      !this->GeneratorTarget->GetPreLinkCommands().empty() ||
+      !this->GeneratorTarget->GetPostBuildCommands().empty()) {
+    return true;
+  }
+
+  for (cmGeneratorTarget::AllConfigSource const& si :
+       this->GeneratorTarget->GetAllConfigSources()) {
+    if (si.Source->GetCustomCommand()) {
+      return true;
+    }
+  }
+
+  return false;
 }
 }
 
 
 void cmVisualStudio10TargetGenerator::WritePackageReferences(Elem& e0)
 void cmVisualStudio10TargetGenerator::WritePackageReferences(Elem& e0)

+ 10 - 0
Source/cmVisualStudio10TargetGenerator.h

@@ -18,6 +18,7 @@
 class cmComputeLinkInformation;
 class cmComputeLinkInformation;
 class cmCustomCommand;
 class cmCustomCommand;
 class cmCustomCommandGenerator;
 class cmCustomCommandGenerator;
+class cmGeneratedFileStream;
 class cmGlobalVisualStudio10Generator;
 class cmGlobalVisualStudio10Generator;
 class cmLocalVisualStudio10Generator;
 class cmLocalVisualStudio10Generator;
 class cmMakefile;
 class cmMakefile;
@@ -260,6 +261,15 @@ private:
   void ClassifyAllConfigSources();
   void ClassifyAllConfigSources();
   void ClassifyAllConfigSource(cmGeneratorTarget::AllConfigSource const& acs);
   void ClassifyAllConfigSource(cmGeneratorTarget::AllConfigSource const& acs);
 
 
+  // .Net SDK-stype project variable and helper functions
+  void WriteClassicMsBuildProjectFile(cmGeneratedFileStream& BuildFileStream);
+  void WriteSdkStyleProjectFile(cmGeneratedFileStream& BuildFileStream);
+
+  void WriteCommonPropertyGroupGlobals(
+    cmVisualStudio10TargetGenerator::Elem& e1);
+
+  bool HasCustomCommands() const;
+
   std::unordered_map<std::string, ConfigToSettings> ParsedToolTargetSettings;
   std::unordered_map<std::string, ConfigToSettings> ParsedToolTargetSettings;
   bool PropertyIsSameInAllConfigs(const ConfigToSettings& toolSettings,
   bool PropertyIsSameInAllConfigs(const ConfigToSettings& toolSettings,
                                   const std::string& propName);
                                   const std::string& propName);

+ 4 - 0
Tests/RunCMake/CMakeLists.txt

@@ -616,6 +616,10 @@ if("${CMAKE_GENERATOR}" MATCHES "Visual Studio ([^9]|9[0-9])")
   endif()
   endif()
 endif()
 endif()
 
 
+if(CMAKE_GENERATOR MATCHES "^Visual Studio (1[6-9]|[2-9][0-9])")
+  add_RunCMake_test(VsDotnetSdk)
+endif()
+
 if(XCODE_VERSION)
 if(XCODE_VERSION)
   add_RunCMake_test(XcodeProject -DXCODE_VERSION=${XCODE_VERSION})
   add_RunCMake_test(XcodeProject -DXCODE_VERSION=${XCODE_VERSION})
   add_RunCMake_test(XcodeProject-Embed -DXCODE_VERSION=${XCODE_VERSION})
   add_RunCMake_test(XcodeProject-Embed -DXCODE_VERSION=${XCODE_VERSION})

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

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

+ 52 - 0
Tests/RunCMake/VsDotnetSdk/DotnetSdkVariables-check.cmake

@@ -0,0 +1,52 @@
+set(files foo.csproj bar.csproj baz.csproj)
+
+set(inLib1 FALSE)
+set(dotnetSdkInLib1 FALSE)
+
+set(inLib2 FALSE)
+set(dotnetSdkWebInLib2 FALSE)
+
+set(inLib3 FALSE)
+set(classicProjInLib3 FALSE)
+
+foreach(file ${files})
+  set(csProjectFile ${RunCMake_TEST_BINARY_DIR}/${file})
+
+  if(NOT EXISTS "${csProjectFile}")
+    set(RunCMake_TEST_FAILED "Project file ${csProjectFile} does not exist.")
+    return()
+  endif()
+
+  file(STRINGS "${csProjectFile}" lines)
+
+  foreach(line IN LISTS lines)
+    if(NOT inLib1)
+      if(line MATCHES "<Project Sdk=\"Microsoft\.NET\.Sdk\">")
+        set(dotnetSdkInLib1 TRUE)
+        set(inLib1  TRUE)
+      endif()
+    elseif(NOT inLib2)
+      if(line MATCHES "<Project Sdk=\"Microsoft\.NET\.Sdk\.Web\">")
+        set(dotnetSdkWebInLib2 TRUE)
+        set(inLib2 TRUE)
+      endif()
+    elseif(NOT inLib3)
+      if(line MATCHES "<Project DefaultTargets=\"Build\" ToolsVersion=\"")
+        set(classicProjInLib3 TRUE)
+        set(inLib3 TRUE)
+      endif()
+    endif()
+  endforeach()
+endforeach()
+
+if(NOT dotnetSdkInLib1)
+  set(RunCMake_TEST_FAILED ".Net SDK not set correctly.")
+endif()
+
+if(NOT dotnetSdkWebInLib2)
+  set(RunCMake_TEST_FAILED ".Net Web SDK not set correctly.")
+endif()
+
+if(NOT classicProjInLib3)
+  set(RunCMake_TEST_FAILED "Empty DOTNET_SDK doesn't build Classic project.")
+endif()

+ 14 - 0
Tests/RunCMake/VsDotnetSdk/DotnetSdkVariables.cmake

@@ -0,0 +1,14 @@
+enable_language(CSharp)
+
+if(NOT CMAKE_CSharp_COMPILER)
+    return()
+endif()
+
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
+add_library(foo SHARED lib1.cs)
+
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk.Web")
+add_library(bar SHARED lib1.cs)
+
+set(CMAKE_DOTNET_SDK "")
+add_library(baz SHARED lib1.cs)

+ 17 - 0
Tests/RunCMake/VsDotnetSdk/RunCMakeTest.cmake

@@ -0,0 +1,17 @@
+cmake_policy(SET CMP0053 NEW)
+include(RunCMake)
+
+run_cmake(VsDotnetSdkCustomCommandsTarget)
+run_cmake(VsDotnetSdkCustomCommandsSource)
+run_cmake(DotnetSdkVariables)
+
+function(run_VsDotnetSdk)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/VsDotnetSdk-build)
+  set(RunCMake_TEST_NO_CLEAN 1)
+  file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+  run_cmake(VsDotnetSdk)
+  set(build_flags /restore)
+  run_cmake_command(VsDotnetSdk-build ${CMAKE_COMMAND} --build . -- ${build_flags})
+endfunction()
+run_VsDotnetSdk()

+ 18 - 0
Tests/RunCMake/VsDotnetSdk/VsDotnetSdk.cmake

@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.22)
+
+# a simple CSharp only test case
+project (DotNetSdk CSharp)
+
+set(CMAKE_DOTNET_TARGET_FRAMEWORK net472)
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
+
+add_library(dotNetSdkLib1 SHARED lib1.cs)
+set_target_properties(dotNetSdkLib1
+    PROPERTIES
+        VS_GLOBAL_RuntimeIdentifier win10-x64)
+
+add_executable(DotNetSdk csharponly.cs)
+target_link_libraries(DotNetSdk dotNetSdkLib1)
+set_target_properties(DotNetSdk
+    PROPERTIES
+        VS_GLOBAL_RuntimeIdentifier win10-x64)

+ 0 - 0
Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource-result.txt


+ 7 - 0
Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource-stderr.txt

@@ -0,0 +1,7 @@
+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.

+ 15 - 0
Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsSource.cmake

@@ -0,0 +1,15 @@
+enable_language(CSharp)
+
+if(NOT CMAKE_CSharp_COMPILER)
+    return()
+endif()
+
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
+add_custom_command(
+  OUTPUT bar.cs
+  COMMAND copy /A ${CMAKE_CURRENT_SOURCE_DIR}/lib1.cs
+             bar.cs
+  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/lib1.cs
+  VERBATIM)
+
+add_library(foo SHARED bar.cs)

+ 0 - 0
Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-result.txt


+ 7 - 0
Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget-stderr.txt

@@ -0,0 +1,7 @@
+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.

+ 12 - 0
Tests/RunCMake/VsDotnetSdk/VsDotnetSdkCustomCommandsTarget.cmake

@@ -0,0 +1,12 @@
+enable_language(CSharp)
+
+if(NOT CMAKE_CSharp_COMPILER)
+    return()
+endif()
+
+set(CMAKE_DOTNET_SDK "Microsoft.NET.Sdk")
+add_library(foo SHARED lib1.cs)
+add_custom_command(TARGET foo
+  PRE_BUILD
+  COMMAND echo "This shouldn't happen!"
+  VERBATIM)

+ 11 - 0
Tests/RunCMake/VsDotnetSdk/csharponly.cs

@@ -0,0 +1,11 @@
+namespace CSharpOnly
+{
+    class CSharpOnly
+    {
+        public static void Main(string[] args)
+        {
+            int val = Lib1.getResult();
+            return;
+        }
+    }
+}

+ 10 - 0
Tests/RunCMake/VsDotnetSdk/lib1.cs

@@ -0,0 +1,10 @@
+namespace CSharpOnly
+{
+    public class Lib1
+    {
+        public static int getResult()
+        {
+            return 23;
+        }
+    }
+}