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
This commit is contained in:
Lauri Vasama
2024-10-27 11:21:52 +02:00
committed by Brad King
parent f1bcb7276a
commit 0bb13ba0e6
16 changed files with 460 additions and 4 deletions

View File

@@ -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`:

View File

@@ -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.

View File

@@ -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``).

View File

@@ -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";
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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";
}
}

View File

@@ -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.

View File

@@ -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()

View File

@@ -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)

View File

@@ -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")