Sfoglia il codice sorgente

VS: Add support for Visual Studio solution items

Files listed in the `VS_SOLUTION_ITEMS` directory property of a project
directory are added as solution items in the 'Solution Items' solution
directory.

If `source_group` is applied to the files listed in `VS_SOLUTION_ITEMS`,
solution groups matching the names of the source groups are created
outside of the default 'Solution Items' group.  If not items are placed
into the default group, it is not created.

Solution items added to subprojects are not included in the top-level
project.

Closes: #26409
Lauri Vasama 1 anno fa
parent
commit
0bb13ba0e6

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

@@ -97,6 +97,7 @@ Properties on Directories
    /prop_dir/VARIABLES
    /prop_dir/VS_GLOBAL_SECTION_POST_section
    /prop_dir/VS_GLOBAL_SECTION_PRE_section
+   /prop_dir/VS_SOLUTION_ITEMS
    /prop_dir/VS_STARTUP_PROJECT
 
 .. _`Target Properties`:

+ 19 - 0
Help/prop_dir/VS_SOLUTION_ITEMS.rst

@@ -0,0 +1,19 @@
+VS_SOLUTION_ITEMS
+-----------------
+
+.. versionadded:: 3.32
+
+Specify solution level items included in the generated Visual Studio solution.
+
+The :ref:`Visual Studio Generators` create a ``.sln`` file for each directory
+whose ``CMakeLists.txt`` file calls the :command:`project` command. Append paths
+to this property in the same directory as the top-level :command:`project`
+command call (e.g. in the top-level ``CMakeLists.txt`` file) to specify files
+included in the corresponding solution file.
+
+If a file specified in ``VS_SOLUTION_ITEMS`` matches a :command:`source_group`
+command call, the affected solution level items are placed in a hierarchy of
+solution level folders according to the name specified in that command.
+Otherwise the items are placed in a default solution level directory named
+``Solution Items``. This name matches the default directory name used by Visual
+Studio when attempting to add solution level items at the root of the solution.

+ 6 - 0
Help/release/dev/vs-solution-items.rst

@@ -0,0 +1,6 @@
+vs-solution-items
+-----------------
+
+* The :prop_dir:`VS_SOLUTION_ITEMS` directory property was added
+  to tell :ref:`Visual Studio Generators` to attach files directly
+  to the Solution (``.sln``).

+ 53 - 0
Source/cmGlobalVisualStudio14Generator.cxx

@@ -541,3 +541,56 @@ std::string cmGlobalVisualStudio14Generator::GetWindows10SDKVersion(
   // Return an empty string
   return std::string();
 }
+
+void cmGlobalVisualStudio14Generator::AddSolutionItems(cmLocalGenerator* root)
+{
+  cmValue n = root->GetMakefile()->GetProperty("VS_SOLUTION_ITEMS");
+  if (cmNonempty(n)) {
+    cmMakefile* makefile = root->GetMakefile();
+
+    std::vector<cmSourceGroup> sourceGroups = makefile->GetSourceGroups();
+
+    cmVisualStudioFolder* defaultFolder = nullptr;
+
+    std::vector<std::string> pathComponents = {
+      makefile->GetCurrentSourceDirectory(),
+      "",
+      "",
+    };
+
+    for (const std::string& relativePath : cmList(n)) {
+      pathComponents[2] = relativePath;
+      std::string fullPath = cmSystemTools::JoinPath(pathComponents);
+
+      cmSourceGroup* sg = makefile->FindSourceGroup(fullPath, sourceGroups);
+
+      cmVisualStudioFolder* folder = nullptr;
+      if (!sg->GetFullName().empty()) {
+        std::string folderPath = sg->GetFullName();
+        // Source groups use '\' while solution folders use '/'.
+        cmSystemTools::ReplaceString(folderPath, "\\", "/");
+        folder = this->CreateSolutionFolders(folderPath);
+      } else {
+        // Lazily initialize the default solution items folder.
+        if (defaultFolder == nullptr) {
+          defaultFolder = this->CreateSolutionFolders("Solution Items");
+        }
+        folder = defaultFolder;
+      }
+
+      folder->SolutionItems.insert(fullPath);
+    }
+  }
+}
+
+void cmGlobalVisualStudio14Generator::WriteFolderSolutionItems(
+  std::ostream& fout, const cmVisualStudioFolder& folder)
+{
+  fout << "\tProjectSection(SolutionItems) = preProject\n";
+
+  for (const std::string& item : folder.SolutionItems) {
+    fout << "\t\t" << item << " = " << item << "\n";
+  }
+
+  fout << "\tEndProjectSection\n";
+}

+ 5 - 0
Source/cmGlobalVisualStudio14Generator.h

@@ -67,6 +67,11 @@ protected:
 
   std::string GetWindows10SDKVersion(cmMakefile* mf);
 
+  void AddSolutionItems(cmLocalGenerator* root) override;
+
+  void WriteFolderSolutionItems(std::ostream& fout,
+                                const cmVisualStudioFolder& folder) override;
+
 private:
   class Factory;
   friend class Factory;

+ 4 - 3
Source/cmGlobalVisualStudio71Generator.cxx

@@ -48,9 +48,10 @@ void cmGlobalVisualStudio71Generator::WriteSLNFile(
   std::ostringstream targetsSlnString;
   this->WriteTargetsToSolution(targetsSlnString, root, orderedProjectTargets);
 
+  this->AddSolutionItems(root);
+
   // Generate folder specification.
-  bool useFolderProperty = this->UseFolderProperty();
-  if (useFolderProperty) {
+  if (!this->VisualStudioFolders.empty()) {
     this->WriteFolders(fout);
   }
 
@@ -67,7 +68,7 @@ void cmGlobalVisualStudio71Generator::WriteSLNFile(
   this->WriteTargetConfigurations(fout, configs, orderedProjectTargets);
   fout << "\tEndGlobalSection\n";
 
-  if (useFolderProperty) {
+  if (!this->VisualStudioFolders.empty()) {
     // Write out project folders
     fout << "\tGlobalSection(NestedProjects) = preSolution\n";
     this->WriteFoldersContent(fout);

+ 7 - 1
Source/cmGlobalVisualStudio7Generator.cxx

@@ -481,7 +481,13 @@ void cmGlobalVisualStudio7Generator::WriteFolders(std::ostream& fout)
     std::string nameOnly = cmSystemTools::GetFilenameName(fullName);
 
     fout << "Project(\"{" << guidProjectTypeFolder << "}\") = \"" << nameOnly
-         << "\", \"" << fullName << "\", \"{" << guid << "}\"\nEndProject\n";
+         << "\", \"" << fullName << "\", \"{" << guid << "}\"\n";
+
+    if (!iter.second.SolutionItems.empty()) {
+      this->WriteFolderSolutionItems(fout, iter.second);
+    }
+
+    fout << "EndProject\n";
   }
 }
 

+ 6 - 0
Source/cmGlobalVisualStudio7Generator.h

@@ -28,6 +28,7 @@ class BT;
 struct cmVisualStudioFolder
 {
   std::set<std::string> Projects;
+  std::set<std::string> SolutionItems;
 };
 
 /** \class cmGlobalVisualStudio7Generator
@@ -185,6 +186,11 @@ protected:
 
   virtual void WriteFolders(std::ostream& fout);
   virtual void WriteFoldersContent(std::ostream& fout);
+
+  virtual void AddSolutionItems(cmLocalGenerator* root) = 0;
+  virtual void WriteFolderSolutionItems(
+    std::ostream& fout, const cmVisualStudioFolder& folder) = 0;
+
   std::map<std::string, cmVisualStudioFolder> VisualStudioFolders;
 
   // Set during OutputSLNFile with the name of the current project.

+ 351 - 0
Tests/RunCMake/VS10Project/SolutionItems-check.cmake

@@ -0,0 +1,351 @@
+function(MapAppend map key value)
+  list(APPEND "${map}_k" "${key}")
+  list(APPEND "${map}_v" "${value}")
+
+  set("${map}_k" "${${map}_k}" PARENT_SCOPE)
+  set("${map}_v" "${${map}_v}" PARENT_SCOPE)
+endfunction()
+
+function(MapLength map out_variable)
+  list(LENGTH "${map}_k" length)
+  set("${out_variable}" "${length}" PARENT_SCOPE)
+endfunction()
+
+function(MapFind map key out_variable)
+  list(FIND "${map}_k" "${key}" index)
+  if("${index}" LESS 0)
+    unset("${out_variable}" PARENT_SCOPE)
+  else()
+    list(GET "${map}_v" "${index}" value)
+    set("${out_variable}" "${value}" PARENT_SCOPE)
+  endif()
+endfunction()
+
+macro(MapPropagateToParentScope map)
+  set("${map}_k" "${${map}_k}" PARENT_SCOPE)
+  set("${map}_v" "${${map}_v}" PARENT_SCOPE)
+endmacro()
+
+
+function(ParseSln vcSlnFile)
+  if(NOT EXISTS "${vcSlnFile}")
+    set(RunCMake_TEST_FAILED "Solution file ${vcSlnFile} does not exist." PARENT_SCOPE)
+    return()
+  endif()
+
+  set(SCOPE "")
+
+  set(IN_SOLUTION_ITEMS FALSE)
+  set(IN_NESTED_PROJECTS FALSE)
+
+  set(REGUID "\\{[0-9A-F-]+\\}")
+
+  file(STRINGS "${vcSlnFile}" lines)
+  foreach(line IN LISTS lines)
+    string(STRIP "${line}" line)
+
+    # Project(...)
+    if(line MATCHES "Project\\(\"(${REGUID})\"\\) = \"([^\"]+)\", \"([^\"]+)\", \"(${REGUID})\"")
+      if(NOT "${SCOPE}" STREQUAL "")
+        set(RunCMake_TEST_FAILED "Improper nesting of Project" PARENT_SCOPE)
+        return()
+      endif()
+      set(SCOPE "Project")
+
+      if("${CMAKE_MATCH_1}" STREQUAL "{2150E333-8FDC-42A3-9474-1A3956D46DE8}")
+        set(GROUP_NAME "${CMAKE_MATCH_2}")
+
+        MapFind(GROUP_PATHS "${GROUP_NAME}" existing_path)
+        if(DEFINED existing_path)
+          set(RunCMake_TEST_FAILED "Duplicate solution items project '${GROUP_NAME}'" PARENT_SCOPE)
+          return()
+        endif()
+
+        MapAppend(GROUP_PATHS "${GROUP_NAME}" "${CMAKE_MATCH_3}")
+        MapAppend(GROUP_GUIDS "${GROUP_NAME}" "${CMAKE_MATCH_4}")
+      endif()
+
+    # EndProject
+    elseif(line STREQUAL "EndProject")
+      if(NOT "${SCOPE}" STREQUAL "Project")
+        set(RunCMake_TEST_FAILED "Improper nesting of EndProject" PARENT_SCOPE)
+        return()
+      endif()
+      set(SCOPE "")
+
+      unset(GROUP_NAME)
+
+    # ProjectSection
+    elseif(line MATCHES "ProjectSection\\(([a-zA-Z]+)\\) = ([a-zA-Z]+)")
+      if(NOT "${SCOPE}" STREQUAL "Project")
+        set(RunCMake_TEST_FAILED "Improper nesting of ProjectSection" PARENT_SCOPE)
+        return()
+      endif()
+      set(SCOPE "ProjectSection")
+
+      if("${CMAKE_MATCH_1}" STREQUAL "SolutionItems")
+        if(NOT "${CMAKE_MATCH_2}" STREQUAL "preProject")
+          set(RunCMake_TEST_FAILED "SolutionItems must be preProject" PARENT_SCOPE)
+          return()
+        endif()
+
+        set(IN_SOLUTION_ITEMS TRUE)
+      endif()
+
+    # EndProjectSection
+    elseif(line STREQUAL "EndProjectSection")
+      if(NOT "${SCOPE}" STREQUAL "ProjectSection")
+        set(RunCMake_TEST_FAILED "Improper nesting of EndProjectSection" PARENT_SCOPE)
+        return()
+      endif()
+      set(SCOPE "Project")
+
+      set(IN_SOLUTION_ITEMS FALSE)
+
+    # Global
+    elseif(line STREQUAL "Global")
+      if(NOT "${SCOPE}" STREQUAL "")
+        set(RunCMake_TEST_FAILED "Improper nesting of Global" PARENT_SCOPE)
+        return()
+      endif()
+      set(SCOPE "Global")
+
+    # EndGlobal
+    elseif(line STREQUAL "EndGlobal")
+      if(NOT "${SCOPE}" STREQUAL "Global")
+        set(RunCMake_TEST_FAILED "Improper nesting of EndGlobal" PARENT_SCOPE)
+        return()
+      endif()
+      set(SCOPE "")
+
+    # GlobalSection
+    elseif(line MATCHES "GlobalSection\\(([a-zA-Z]+)\\) = ([a-zA-Z]+)")
+      if(NOT "${SCOPE}" STREQUAL "Global")
+        set(RunCMake_TEST_FAILED "Improper nesting of GlobalSection" PARENT_SCOPE)
+        return()
+      endif()
+      set(SCOPE "GlobalSection")
+
+      if("${CMAKE_MATCH_1}" STREQUAL "NestedProjects")
+        if(NOT "${CMAKE_MATCH_2}" STREQUAL "preSolution")
+          set(RunCMake_TEST_FAILED "NestedProjects must be preSolution" PARENT_SCOPE)
+          return()
+        endif()
+
+        set(IN_NESTED_PROJECTS TRUE)
+      endif()
+
+    # EndGlobalSection
+    elseif(line STREQUAL "EndGlobalSection")
+      if(NOT "${SCOPE}" STREQUAL "GlobalSection")
+        set(RunCMake_TEST_FAILED "Improper nesting of EndGlobalSection" PARENT_SCOPE)
+        return()
+      endif()
+      set(SCOPE "Global")
+
+      set(IN_NESTED_PROJECTS FALSE)
+
+    # .../solution-item-0-1.txt = .../solution-item-0-1.txt
+    elseif(${IN_SOLUTION_ITEMS})
+      if(NOT line MATCHES "([^=]+)=([^=]+)")
+        set(RunCMake_TEST_FAILED "Invalid solution item paths 1" PARENT_SCOPE)
+        return()
+      endif()
+
+      string(STRIP "${CMAKE_MATCH_1}" CMAKE_MATCH_1)
+      string(STRIP "${CMAKE_MATCH_2}" CMAKE_MATCH_2)
+
+      if(NOT "${CMAKE_MATCH_1}" STREQUAL "${CMAKE_MATCH_2}")
+        set(RunCMake_TEST_FAILED "Invalid solution item paths 2" PARENT_SCOPE)
+        return()
+      endif()
+
+      cmake_path(GET CMAKE_MATCH_1 FILENAME filename)
+      MapAppend(SOLUTION_ITEMS "${filename}" "${GROUP_NAME}")
+
+    # {1EB55F5E...} = {A11E84C6...}
+    elseif(${IN_NESTED_PROJECTS})
+      if(NOT line MATCHES "(${REGUID}) = (${REGUID})")
+        set(RunCMake_TEST_FAILED "Invalid nested project guids" PARENT_SCOPE)
+        return()
+      endif()
+
+      MapFind(PROJECT_PARENTS "${CMAKE_MATCH_1}" existing_parent)
+      if(DEFINED existing_parent)
+        set(RunCMake_TEST_FAILED "Duplicate nested project: '${CMAKE_MATCH_1}'" PARENT_SCOPE)
+        return()
+      endif()
+
+      MapAppend(PROJECT_PARENTS "${CMAKE_MATCH_1}" "${CMAKE_MATCH_2}")
+
+    endif()
+
+    MapPropagateToParentScope(GROUP_PATHS)
+    MapPropagateToParentScope(GROUP_GUIDS)
+    MapPropagateToParentScope(PROJECT_PARENTS)
+    MapPropagateToParentScope(SOLUTION_ITEMS)
+  endforeach()
+endfunction()
+
+
+# Check the root solution:
+block()
+  ParseSln("${RunCMake_TEST_BINARY_DIR}/SolutionItems.sln")
+
+  if(DEFINED RunCMake_TEST_FAILED)
+    set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
+    return()
+  endif()
+
+
+  # Check group guids and nesting:
+
+  MapFind(GROUP_GUIDS "Solution Items" root_group_guid)
+  if(NOT DEFINED root_group_guid)
+    set(RunCMake_TEST_FAILED "Solution Items not found" PARENT_SCOPE)
+    return()
+  endif()
+  MapFind(GROUP_PATHS "Solution Items" root_group_path)
+  if(NOT "${root_group_path}" STREQUAL "Solution Items")
+    set(RunCMake_TEST_FAILED "Invalid Solution Items path: '${root_group_path}'" PARENT_SCOPE)
+    return()
+  endif()
+  MapFind(PROJECT_PARENTS "${root_group_guid}" root_group_parent_guid)
+  if(DEFINED root_group_parent_guid)
+    set(RunCMake_TEST_FAILED "Solution Items is nested" PARENT_SCOPE)
+    return()
+  endif()
+
+  MapFind(GROUP_GUIDS "Outer Group" outer_group_guid)
+  if(NOT DEFINED outer_group_guid)
+    set(RunCMake_TEST_FAILED "Outer Group not found" PARENT_SCOPE)
+    return()
+  endif()
+  MapFind(GROUP_PATHS "Outer Group" outer_group_path)
+  if(NOT "${outer_group_path}" STREQUAL "Outer Group")
+    set(RunCMake_TEST_FAILED "Invalid Outer Group path: '${outer_group_path}'" PARENT_SCOPE)
+    return()
+  endif()
+  MapFind(PROJECT_PARENTS "${outer_group_guid}" outer_group_parent_guid)
+  if(DEFINED outer_group_parent_guid)
+    set(RunCMake_TEST_FAILED "Outer Group is nested" PARENT_SCOPE)
+    return()
+  endif()
+
+  MapFind(GROUP_GUIDS "Inner Group" inner_group_guid)
+  if(NOT DEFINED inner_group_guid)
+    set(RunCMake_TEST_FAILED "Inner Group not found" PARENT_SCOPE)
+    return()
+  endif()
+  MapFind(GROUP_PATHS "Inner Group" inner_group_path)
+  if(NOT "${inner_group_path}" STREQUAL "Outer Group\\Inner Group")
+    set(RunCMake_TEST_FAILED "Invalid Inner Group path: '${inner_group_path}'" PARENT_SCOPE)
+    return()
+  endif()
+  MapFind(PROJECT_PARENTS "${inner_group_guid}" inner_group_parent_guid)
+  if(NOT DEFINED inner_group_parent_guid)
+    set(RunCMake_TEST_FAILED "Inner Group is not nested" PARENT_SCOPE)
+    return()
+  endif()
+  if(NOT "${inner_group_parent_guid}" STREQUAL "${outer_group_guid}")
+    set(RunCMake_TEST_FAILED "Inner Group is not nested within Outer Group" PARENT_SCOPE)
+    return()
+  endif()
+
+
+  # Check solution items and nesting:
+  MapLength(SOLUTION_ITEMS solution_item_count)
+  if(NOT "${solution_item_count}" EQUAL 4)
+    set(RunCMake_TEST_FAILED "Unexpected number of solution items: ${solution_item_count}")
+    return()
+  endif()
+
+  MapFind(SOLUTION_ITEMS "solution-item-0-1.txt" group_name)
+  if(NOT DEFINED group_name)
+    set(RunCMake_TEST_FAILED "Solution item not found: solution-item-0-1.txt")
+    return()
+  endif()
+  if(NOT "${group_name}" STREQUAL "Solution Items")
+    set(RunCMake_TEST_FAILED "Invalid group for solution-item-0-1.txt: '${group_name}'")
+    return()
+  endif()
+
+  MapFind(SOLUTION_ITEMS "solution-item-1-1.txt" group_name)
+  if(NOT DEFINED group_name)
+    set(RunCMake_TEST_FAILED "Solution item not found: solution-item-1-1.txt")
+    return()
+  endif()
+  if(NOT "${group_name}" STREQUAL "Outer Group")
+    set(RunCMake_TEST_FAILED "Invalid group for solution-item-1-1.txt: '${group_name}'")
+    return()
+  endif()
+
+  MapFind(SOLUTION_ITEMS "solution-item-2-1.txt" group_name)
+  if(NOT DEFINED group_name)
+    set(RunCMake_TEST_FAILED "Solution item not found: solution-item-2-1.txt")
+    return()
+  endif()
+  if(NOT "${group_name}" STREQUAL "Inner Group")
+    set(RunCMake_TEST_FAILED "Invalid group for solution-item-2-1.txt: '${group_name}'")
+    return()
+  endif()
+
+  MapFind(SOLUTION_ITEMS "solution-item-2-2.txt" group_name)
+  if(NOT DEFINED group_name)
+    set(RunCMake_TEST_FAILED "Solution item not found: solution-item-2-2.txt")
+    return()
+  endif()
+  if(NOT "${group_name}" STREQUAL "Inner Group")
+    set(RunCMake_TEST_FAILED "Invalid group for solution-item-2-2.txt: '${group_name}'")
+    return()
+  endif()
+endblock()
+
+
+# Check the nested solution:
+block()
+  ParseSln("${RunCMake_TEST_BINARY_DIR}/SolutionItems/SolutionItemsSubproject.sln")
+
+  if(DEFINED RunCMake_TEST_FAILED)
+    set(RunCMake_TEST_FAILED "${RunCMake_TEST_FAILED}" PARENT_SCOPE)
+    return()
+  endif()
+
+
+  # Check group guids and nesting:
+
+  MapFind(GROUP_GUIDS "Extraneous" root_group_guid)
+  if(NOT DEFINED root_group_guid)
+    set(RunCMake_TEST_FAILED "Extraneous not found" PARENT_SCOPE)
+    return()
+  endif()
+  MapFind(GROUP_PATHS "Extraneous" root_group_path)
+  if(NOT "${root_group_path}" STREQUAL "Extraneous")
+    set(RunCMake_TEST_FAILED "Invalid Extraneous path: '${root_group_path}'" PARENT_SCOPE)
+    return()
+  endif()
+  MapFind(PROJECT_PARENTS "${root_group_guid}" root_group_parent_guid)
+  if(DEFINED root_group_parent_guid)
+    set(RunCMake_TEST_FAILED "Extraneous is nested" PARENT_SCOPE)
+    return()
+  endif()
+
+
+  # Check solution items and nesting:
+
+  MapLength(SOLUTION_ITEMS solution_item_count)
+  if(NOT "${solution_item_count}" EQUAL 1)
+    set(RunCMake_TEST_FAILED "Unexpected number of solution items: ${solution_item_count}" PARENT_SCOPE)
+    return()
+  endif()
+
+  MapFind(SOLUTION_ITEMS "extraneous.txt" group_name)
+  if(NOT DEFINED group_name)
+    set(RunCMake_TEST_FAILED "Solution item not found: extraneous.txt" PARENT_SCOPE)
+    return()
+  endif()
+  if(NOT "${group_name}" STREQUAL "Extraneous")
+    set(RunCMake_TEST_FAILED "Invalid group for extraneous.txt: '${group_name}'" PARENT_SCOPE)
+    return()
+  endif()
+endblock()

+ 4 - 0
Tests/RunCMake/VS10Project/SolutionItems.cmake

@@ -0,0 +1,4 @@
+set_property(DIRECTORY APPEND PROPERTY VS_SOLUTION_ITEMS solution-item-0-1.txt solution-item-1-1.txt solution-item-2-1.txt solution-item-2-2.txt)
+source_group("Outer Group" FILES solution-item-1-1.txt)
+source_group("Outer Group/Inner Group" REGULAR_EXPRESSION "solution-item-2-[0-9]+\\.txt")
+add_subdirectory(SolutionItems)

+ 4 - 0
Tests/RunCMake/VS10Project/SolutionItems/CMakeLists.txt

@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.10)
+project(SolutionItemsSubproject)
+set_property(DIRECTORY APPEND PROPERTY VS_SOLUTION_ITEMS extraneous.txt)
+source_group("Extraneous" REGULAR_EXPRESSION "[^.]+\\.txt")

+ 0 - 0
Tests/RunCMake/VS10Project/SolutionItems/extraneous.txt


+ 0 - 0
Tests/RunCMake/VS10Project/solution-item-0-1.txt


+ 0 - 0
Tests/RunCMake/VS10Project/solution-item-1-1.txt


+ 0 - 0
Tests/RunCMake/VS10Project/solution-item-2-1.txt


+ 0 - 0
Tests/RunCMake/VS10Project/solution-item-2-2.txt