Browse Source

VS: Decouple solution generation from `.sln` file format

Rewrite `.sln` generation using a structured intermediate representation.

Issue: #25887
Brad King 1 month ago
parent
commit
3882718872

+ 2 - 0
Source/CMakeLists.txt

@@ -975,6 +975,8 @@ if(WIN32)
         cmVisualStudioWCEPlatformParser.cxx
         cmVSSetupHelper.cxx
         cmVSSetupHelper.h
+        cmVSSolution.cxx
+        cmVSSolution.h
         cmVSVersion.h
       )
 

+ 0 - 57
Source/cmGlobalVisualStudio14Generator.cxx

@@ -521,60 +521,3 @@ std::string cmGlobalVisualStudio14Generator::GetWindows10SDKVersion(
   // Return an empty string
   return std::string();
 }
-
-void cmGlobalVisualStudio14Generator::AddSolutionItems(cmLocalGenerator* root,
-                                                       VSFolders& vsFolders)
-{
-  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 (std::string const& relativePath : cmList(n)) {
-      pathComponents[2] = relativePath;
-
-      std::string fullPath = cmSystemTools::FileIsFullPath(relativePath)
-        ? relativePath
-        : 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 = vsFolders.Create(folderPath);
-      } else {
-        // Lazily initialize the default solution items folder.
-        if (defaultFolder == nullptr) {
-          defaultFolder = vsFolders.Create("Solution Items");
-        }
-        folder = defaultFolder;
-      }
-
-      folder->SolutionItems.insert(fullPath);
-    }
-  }
-}
-
-void cmGlobalVisualStudio14Generator::WriteFolderSolutionItems(
-  std::ostream& fout, cmVisualStudioFolder const& folder) const
-{
-  fout << "\tProjectSection(SolutionItems) = preProject\n";
-
-  for (std::string const& item : folder.SolutionItems) {
-    fout << "\t\t" << item << " = " << item << "\n";
-  }
-
-  fout << "\tEndProjectSection\n";
-}

+ 0 - 5
Source/cmGlobalVisualStudio14Generator.h

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

+ 0 - 158
Source/cmGlobalVisualStudio71Generator.cxx

@@ -2,167 +2,9 @@
    file LICENSE.rst or https://cmake.org/licensing for details.  */
 #include "cmGlobalVisualStudio71Generator.h"
 
-#include <map>
-#include <sstream>
-
-#include "cmGeneratorTarget.h"
-#include "cmGlobalGenerator.h"
-#include "cmGlobalVisualStudioGenerator.h"
-#include "cmList.h"
-#include "cmListFileCache.h"
-#include "cmLocalGenerator.h"
-#include "cmMakefile.h"
-#include "cmStringAlgorithms.h"
-#include "cmSystemTools.h"
-
 class cmake;
 
 cmGlobalVisualStudio71Generator::cmGlobalVisualStudio71Generator(cmake* cm)
   : cmGlobalVisualStudio7Generator(cm)
 {
-  this->ProjectConfigurationSectionName = "ProjectConfiguration";
-}
-
-void cmGlobalVisualStudio71Generator::WriteSLNFile(
-  std::ostream& fout, cmLocalGenerator* root,
-  OrderedTargetDependSet const& orderedProjectTargets,
-  VSFolders const& vsFolders) const
-{
-  std::vector<std::string> configs =
-    root->GetMakefile()->GetGeneratorConfigs(cmMakefile::ExcludeEmptyConfig);
-
-  // Write out the header for a SLN file
-  this->WriteSLNHeader(fout);
-
-  // Generate folder specification.
-  if (!vsFolders.Folders.empty()) {
-    this->WriteFolders(fout, vsFolders);
-  }
-
-  // Now write the actual target specification content.
-  this->WriteTargetsToSolution(fout, root, orderedProjectTargets);
-
-  // Write out the configurations information for the solution
-  fout << "Global\n";
-  // Write out the configurations for the solution
-  this->WriteSolutionConfigurations(fout, configs);
-  fout << "\tGlobalSection(" << this->ProjectConfigurationSectionName
-       << ") = postSolution\n";
-  // Write out the configurations for all the targets in the project
-  this->WriteTargetConfigurations(fout, configs, orderedProjectTargets);
-  fout << "\tEndGlobalSection\n";
-
-  if (!vsFolders.Folders.empty()) {
-    // Write out project folders
-    fout << "\tGlobalSection(NestedProjects) = preSolution\n";
-    this->WriteFoldersContent(fout, vsFolders);
-    fout << "\tEndGlobalSection\n";
-  }
-
-  // Write out global sections
-  this->WriteSLNGlobalSections(fout, root);
-
-  // Write the footer for the SLN file
-  this->WriteSLNFooter(fout);
-}
-
-// Write a dsp file into the SLN file,
-// Note, that dependencies from executables to
-// the libraries it uses are also done here
-void cmGlobalVisualStudio71Generator::WriteProject(
-  std::ostream& fout, std::string const& dspname, std::string const& dir,
-  cmGeneratorTarget const* t) const
-{
-  // check to see if this is a fortran build
-  std::string ext = ".vcproj";
-  char const* project =
-    R"(Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = ")";
-  if (this->TargetIsFortranOnly(t)) {
-    ext = ".vfproj";
-    project = R"(Project("{6989167D-11E4-40FE-8C1A-2192A86A7E90}") = ")";
-  }
-  if (t->IsCSharpOnly()) {
-    ext = ".csproj";
-    project = R"(Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = ")";
-  }
-  cmValue targetExt = t->GetProperty("GENERATOR_FILE_NAME_EXT");
-  if (targetExt) {
-    ext = *targetExt;
-  }
-
-  std::string guid = this->GetGUID(dspname);
-  fout << project << dspname << "\", \"" << this->ConvertToSolutionPath(dir)
-       << (!dir.empty() ? "\\" : "") << dspname << ext << "\", \"{" << guid
-       << "}\"\n";
-  fout << "\tProjectSection(ProjectDependencies) = postProject\n";
-  this->WriteProjectDepends(fout, dspname, dir, t);
-  fout << "\tEndProjectSection\n";
-
-  fout << "EndProject\n";
-}
-
-// Write a dsp file into the SLN file, Note, that dependencies from
-// executables to the libraries it uses are also done here
-void cmGlobalVisualStudio71Generator::WriteExternalProject(
-  std::ostream& fout, std::string const& name, std::string const& location,
-  cmValue typeGuid,
-  std::set<BT<std::pair<std::string, bool>>> const& depends) const
-{
-  fout << "Project(\"{"
-       << (typeGuid ? *typeGuid
-                    : std::string(
-                        cmGlobalVisualStudio71Generator::ExternalProjectType(
-                          location)))
-       << "}\") = \"" << name << "\", \""
-       << this->ConvertToSolutionPath(location) << "\", \"{"
-       << this->GetGUID(name) << "}\"\n";
-
-  // write out the dependencies here VS 7.1 includes dependencies with the
-  // project instead of in the global section
-  if (!depends.empty()) {
-    fout << "\tProjectSection(ProjectDependencies) = postProject\n";
-    for (BT<std::pair<std::string, bool>> const& it : depends) {
-      std::string const& dep = it.Value.first;
-      if (this->IsDepInSolution(dep)) {
-        fout << "\t\t{" << this->GetGUID(dep) << "} = {" << this->GetGUID(dep)
-             << "}\n";
-      }
-    }
-    fout << "\tEndProjectSection\n";
-  }
-
-  fout << "EndProject\n";
-}
-
-// Write a dsp file into the SLN file, Note, that dependencies from
-// executables to the libraries it uses are also done here
-void cmGlobalVisualStudio71Generator::WriteProjectConfigurations(
-  std::ostream& fout, std::string const& name, cmGeneratorTarget const& target,
-  std::vector<std::string> const& configs,
-  std::set<std::string> const& configsPartOfDefaultBuild,
-  std::string const& platformMapping) const
-{
-  std::string const& platformName =
-    !platformMapping.empty() ? platformMapping : this->GetPlatformName();
-  std::string guid = this->GetGUID(name);
-  for (std::string const& i : configs) {
-    cmList mapConfig;
-    char const* dstConfig = i.c_str();
-    if (target.GetProperty("EXTERNAL_MSPROJECT")) {
-      if (cmValue m = target.GetProperty(
-            cmStrCat("MAP_IMPORTED_CONFIG_", cmSystemTools::UpperCase(i)))) {
-        mapConfig.assign(*m);
-        if (!mapConfig.empty()) {
-          dstConfig = mapConfig[0].c_str();
-        }
-      }
-    }
-    fout << "\t\t{" << guid << "}." << i << ".ActiveCfg = " << dstConfig << '|'
-         << platformName << std::endl;
-    auto ci = configsPartOfDefaultBuild.find(i);
-    if (!(ci == configsPartOfDefaultBuild.end())) {
-      fout << "\t\t{" << guid << "}." << i << ".Build.0 = " << dstConfig << '|'
-           << platformName << std::endl;
-    }
-  }
 }

+ 0 - 37
Source/cmGlobalVisualStudio71Generator.h

@@ -2,49 +2,12 @@
    file LICENSE.rst or https://cmake.org/licensing for details.  */
 #pragma once
 
-#include <iosfwd>
-#include <set>
-#include <string>
-#include <utility>
-#include <vector>
-
 #include "cmGlobalVisualStudio7Generator.h"
-#include "cmValue.h"
 
-class cmGeneratorTarget;
-class cmLocalGenerator;
 class cmake;
-template <typename T>
-class BT;
 
-/** \class cmGlobalVisualStudio71Generator
- * \brief Write a Unix makefiles.
- *
- * cmGlobalVisualStudio71Generator manages UNIX build process for a tree
- */
 class cmGlobalVisualStudio71Generator : public cmGlobalVisualStudio7Generator
 {
 public:
   cmGlobalVisualStudio71Generator(cmake* cm);
-
-protected:
-  void WriteSLNFile(std::ostream& fout, cmLocalGenerator* root,
-                    OrderedTargetDependSet const& orderedProjectTargets,
-                    VSFolders const& vsFolders) const override;
-  virtual void WriteSolutionConfigurations(
-    std::ostream& fout, std::vector<std::string> const& configs) const = 0;
-  void WriteProject(std::ostream& fout, std::string const& name,
-                    std::string const& path,
-                    cmGeneratorTarget const* t) const override;
-  void WriteProjectConfigurations(
-    std::ostream& fout, std::string const& name,
-    cmGeneratorTarget const& target, std::vector<std::string> const& configs,
-    std::set<std::string> const& configsPartOfDefaultBuild,
-    std::string const& platformMapping = "") const override;
-  void WriteExternalProject(
-    std::ostream& fout, std::string const& name, std::string const& path,
-    cmValue typeGuid,
-    std::set<BT<std::pair<std::string, bool>>> const& depends) const override;
-
-  std::string ProjectConfigurationSectionName;
 };

+ 1 - 250
Source/cmGlobalVisualStudio7Generator.cxx

@@ -28,7 +28,6 @@
 #include "cmSystemTools.h"
 #include "cmTarget.h"
 #include "cmTargetDepend.h"
-#include "cmUuid.h"
 #include "cmVisualStudioGeneratorOptions.h"
 #include "cmake.h"
 
@@ -344,11 +343,6 @@ void cmGlobalVisualStudio7Generator::OutputSLNFile(
   // closure of their dependencies.
   TargetDependSet const projectTargets =
     this->GetTargetsForProject(root, generators);
-  OrderedTargetDependSet orderedProjectTargets(
-    projectTargets, this->GetStartupProjectName(root));
-
-  VSFolders vsFolders = this->CreateSolutionFolders(orderedProjectTargets);
-  this->AddSolutionItems(root, vsFolders);
 
   std::string fname = GetSLNFile(root);
   cmGeneratedFileStream fout(fname);
@@ -356,255 +350,12 @@ void cmGlobalVisualStudio7Generator::OutputSLNFile(
   if (!fout) {
     return;
   }
-  this->WriteSLNFile(fout, root, orderedProjectTargets, vsFolders);
+  this->WriteSLNFile(fout, root, projectTargets);
   if (fout.Close()) {
     this->FileReplacedDuringGenerate(fname);
   }
 }
 
-void cmGlobalVisualStudio7Generator::WriteTargetConfigurations(
-  std::ostream& fout, std::vector<std::string> const& configs,
-  OrderedTargetDependSet const& projectTargets) const
-{
-  // loop over again and write out configurations for each target
-  // in the solution
-  for (cmGeneratorTarget const* target : projectTargets) {
-    if (!this->IsInSolution(target)) {
-      continue;
-    }
-    cmValue expath = target->GetProperty("EXTERNAL_MSPROJECT");
-    if (expath) {
-      std::set<std::string> allConfigurations(configs.begin(), configs.end());
-      cmValue mapping = target->GetProperty("VS_PLATFORM_MAPPING");
-      this->WriteProjectConfigurations(fout, target->GetName(), *target,
-                                       configs, allConfigurations,
-                                       mapping ? *mapping : "");
-    } else {
-      std::set<std::string> const& configsPartOfDefaultBuild =
-        this->IsPartOfDefaultBuild(configs, projectTargets, target);
-      cmValue vcprojName = target->GetProperty("GENERATOR_FILE_NAME");
-      if (vcprojName) {
-        std::string mapping;
-
-        // On VS 19 and above, always map .NET SDK projects to "Any CPU".
-        if (target->IsDotNetSdkTarget() && this->Version >= VSVersion::VS16 &&
-            !cmGlobalVisualStudio7Generator::IsReservedTarget(
-              target->GetName())) {
-          mapping = "Any CPU";
-        }
-        this->WriteProjectConfigurations(fout, *vcprojName, *target, configs,
-                                         configsPartOfDefaultBuild, mapping);
-      }
-    }
-  }
-}
-
-cmGlobalVisualStudio7Generator::VSFolders
-cmGlobalVisualStudio7Generator::CreateSolutionFolders(
-  OrderedTargetDependSet const& orderedProjectTargets)
-{
-  VSFolders vsFolders;
-  if (!this->UseFolderProperty()) {
-    return vsFolders;
-  }
-  for (cmGeneratorTarget const* target : orderedProjectTargets) {
-    if (this->IsInSolution(target) &&
-        (target->GetProperty("EXTERNAL_MSPROJECT") ||
-         target->GetProperty("GENERATOR_FILE_NAME"))) {
-      // Create "solution folder" information from FOLDER target property
-      if (cmVisualStudioFolder* folder =
-            vsFolders.Create(target->GetEffectiveFolderName())) {
-        folder->Projects.insert(target->GetName());
-      }
-    }
-  }
-  return vsFolders;
-}
-
-cmVisualStudioFolder* cmGlobalVisualStudio7Generator::VSFolders::Create(
-  std::string const& path)
-{
-  if (path.empty()) {
-    return nullptr;
-  }
-
-  std::vector<std::string> tokens =
-    cmSystemTools::SplitString(path, '/', false);
-
-  std::string cumulativePath;
-
-  for (std::string const& iter : tokens) {
-    if (iter.empty()) {
-      continue;
-    }
-
-    if (cumulativePath.empty()) {
-      cumulativePath = cmStrCat("CMAKE_FOLDER_GUID_", iter);
-    } else {
-      this->Folders[cumulativePath].Projects.insert(
-        cmStrCat(cumulativePath, '/', iter));
-
-      cumulativePath = cmStrCat(cumulativePath, '/', iter);
-    }
-  }
-
-  if (cumulativePath.empty()) {
-    return nullptr;
-  }
-
-  return &this->Folders[cumulativePath];
-}
-
-void cmGlobalVisualStudio7Generator::WriteTargetsToSolution(
-  std::ostream& fout, cmLocalGenerator* root,
-  OrderedTargetDependSet const& projectTargets) const
-{
-  std::vector<std::string> configs =
-    root->GetMakefile()->GetGeneratorConfigs(cmMakefile::ExcludeEmptyConfig);
-
-  for (cmGeneratorTarget const* target : projectTargets) {
-    if (!this->IsInSolution(target)) {
-      continue;
-    }
-    // handle external vc project files
-    cmValue expath = target->GetProperty("EXTERNAL_MSPROJECT");
-    if (expath) {
-      std::string project = target->GetName();
-      std::string const& location = *expath;
-
-      this->WriteExternalProject(fout, project, location,
-                                 target->GetProperty("VS_PROJECT_TYPE"),
-                                 target->GetUtilities());
-    } else {
-      cmValue vcprojName = target->GetProperty("GENERATOR_FILE_NAME");
-      if (vcprojName) {
-        cmLocalGenerator* lg = target->GetLocalGenerator();
-        std::string dir = lg->GetCurrentBinaryDirectory();
-        dir = root->MaybeRelativeToCurBinDir(dir);
-        if (dir == "."_s) {
-          dir.clear(); // msbuild cannot handle ".\" prefix
-        }
-        this->WriteProject(fout, *vcprojName, dir, target);
-      }
-    }
-  }
-}
-
-void cmGlobalVisualStudio7Generator::WriteFolders(
-  std::ostream& fout, VSFolders const& vsFolders) const
-{
-  cm::string_view const prefix = "CMAKE_FOLDER_GUID_";
-  std::string guidProjectTypeFolder = "2150E333-8FDC-42A3-9474-1A3956D46DE8";
-  for (auto const& iter : vsFolders.Folders) {
-    std::string fullName = iter.first;
-    std::string guid = this->GetGUID(fullName);
-
-    std::replace(fullName.begin(), fullName.end(), '/', '\\');
-    if (cmHasPrefix(fullName, prefix)) {
-      fullName = fullName.substr(prefix.size());
-    }
-
-    std::string nameOnly = cmSystemTools::GetFilenameName(fullName);
-
-    fout << "Project(\"{" << guidProjectTypeFolder << "}\") = \"" << nameOnly
-         << "\", \"" << fullName << "\", \"{" << guid << "}\"\n";
-
-    if (!iter.second.SolutionItems.empty()) {
-      this->WriteFolderSolutionItems(fout, iter.second);
-    }
-
-    fout << "EndProject\n";
-  }
-}
-
-void cmGlobalVisualStudio7Generator::WriteFoldersContent(
-  std::ostream& fout, VSFolders const& vsFolders) const
-{
-  for (auto const& iter : vsFolders.Folders) {
-    std::string key(iter.first);
-    std::string guidParent(this->GetGUID(key));
-
-    for (std::string const& it : iter.second.Projects) {
-      std::string const& value(it);
-      std::string guid(this->GetGUID(value));
-
-      fout << "\t\t{" << guid << "} = {" << guidParent << "}\n";
-    }
-  }
-}
-
-void cmGlobalVisualStudio7Generator::WriteSLNGlobalSections(
-  std::ostream& fout, cmLocalGenerator* root) const
-{
-  std::string const guid =
-    this->GetGUID(cmStrCat(root->GetProjectName(), ".sln"));
-  bool extensibilityGlobalsOverridden = false;
-  bool extensibilityAddInsOverridden = false;
-  std::vector<std::string> const propKeys =
-    root->GetMakefile()->GetPropertyKeys();
-  for (std::string const& it : propKeys) {
-    if (cmHasLiteralPrefix(it, "VS_GLOBAL_SECTION_")) {
-      std::string sectionType;
-      std::string name = it.substr(18);
-      if (cmHasLiteralPrefix(name, "PRE_")) {
-        name = name.substr(4);
-        sectionType = "preSolution";
-      } else if (cmHasLiteralPrefix(name, "POST_")) {
-        name = name.substr(5);
-        sectionType = "postSolution";
-      } else {
-        continue;
-      }
-      if (!name.empty()) {
-        bool addGuid = false;
-        if (name == "ExtensibilityGlobals"_s &&
-            sectionType == "postSolution"_s) {
-          addGuid = true;
-          extensibilityGlobalsOverridden = true;
-        } else if (name == "ExtensibilityAddIns"_s &&
-                   sectionType == "postSolution"_s) {
-          extensibilityAddInsOverridden = true;
-        }
-        fout << "\tGlobalSection(" << name << ") = " << sectionType << '\n';
-        cmValue p = root->GetMakefile()->GetProperty(it);
-        cmList keyValuePairs{ *p };
-        for (std::string const& itPair : keyValuePairs) {
-          std::string::size_type const posEqual = itPair.find('=');
-          if (posEqual != std::string::npos) {
-            std::string const key =
-              cmTrimWhitespace(itPair.substr(0, posEqual));
-            std::string const value =
-              cmTrimWhitespace(itPair.substr(posEqual + 1));
-            fout << "\t\t" << key << " = " << value << '\n';
-            if (key == "SolutionGuid"_s) {
-              addGuid = false;
-            }
-          }
-        }
-        if (addGuid) {
-          fout << "\t\tSolutionGuid = {" << guid << "}\n";
-        }
-        fout << "\tEndGlobalSection\n";
-      }
-    }
-  }
-  if (!extensibilityGlobalsOverridden) {
-    fout << "\tGlobalSection(ExtensibilityGlobals) = postSolution\n"
-         << "\t\tSolutionGuid = {" << guid << "}\n"
-         << "\tEndGlobalSection\n";
-  }
-  if (!extensibilityAddInsOverridden) {
-    fout << "\tGlobalSection(ExtensibilityAddIns) = postSolution\n"
-         << "\tEndGlobalSection\n";
-  }
-}
-
-// Standard end of dsw file
-void cmGlobalVisualStudio7Generator::WriteSLNFooter(std::ostream& fout) const
-{
-  fout << "EndGlobal\n";
-}
-
 void cmGlobalVisualStudio7Generator::AppendDirectoryForConfig(
   std::string const& prefix, std::string const& config,
   std::string const& suffix, std::string& dir)

+ 0 - 55
Source/cmGlobalVisualStudio7Generator.h

@@ -25,12 +25,6 @@ class cmake;
 template <typename T>
 class BT;
 
-struct cmVisualStudioFolder
-{
-  std::set<std::string> Projects;
-  std::set<std::string> SolutionItems;
-};
-
 /** \class cmGlobalVisualStudio7Generator
  * \brief Write a Unix makefiles.
  *
@@ -126,12 +120,6 @@ protected:
 
   void Generate() override;
 
-  struct VSFolders
-  {
-    std::map<std::string, cmVisualStudioFolder> Folders;
-    cmVisualStudioFolder* Create(std::string const& path);
-  };
-
   std::string const& GetDevEnvCommand();
   virtual std::string FindDevEnvCommand();
 
@@ -139,49 +127,6 @@ protected:
 
   virtual void OutputSLNFile(cmLocalGenerator* root,
                              std::vector<cmLocalGenerator*>& generators);
-  virtual void WriteSLNFile(
-    std::ostream& fout, cmLocalGenerator* root,
-    OrderedTargetDependSet const& orderedProjectTargets,
-    VSFolders const& vsFolders) const = 0;
-  virtual void WriteProject(std::ostream& fout, std::string const& name,
-                            std::string const& path,
-                            cmGeneratorTarget const* t) const = 0;
-  virtual void WriteProjectDepends(std::ostream& fout, std::string const& name,
-                                   std::string const& path,
-                                   cmGeneratorTarget const* t) const = 0;
-  virtual void WriteProjectConfigurations(
-    std::ostream& fout, std::string const& name,
-    cmGeneratorTarget const& target, std::vector<std::string> const& configs,
-    std::set<std::string> const& configsPartOfDefaultBuild,
-    std::string const& platformMapping = "") const = 0;
-  virtual void WriteSLNGlobalSections(std::ostream& fout,
-                                      cmLocalGenerator* root) const;
-  virtual void WriteSLNFooter(std::ostream& fout) const;
-
-  VSFolders CreateSolutionFolders(
-    OrderedTargetDependSet const& orderedProjectTargets);
-
-  virtual void WriteTargetsToSolution(
-    std::ostream& fout, cmLocalGenerator* root,
-    OrderedTargetDependSet const& projectTargets) const;
-  virtual void WriteTargetConfigurations(
-    std::ostream& fout, std::vector<std::string> const& configs,
-    OrderedTargetDependSet const& projectTargets) const;
-
-  virtual void WriteExternalProject(
-    std::ostream& fout, std::string const& name, std::string const& path,
-    cmValue typeGuid,
-    std::set<BT<std::pair<std::string, bool>>> const& dependencies) const = 0;
-
-  virtual void WriteFolders(std::ostream& fout,
-                            VSFolders const& vsFolders) const;
-  virtual void WriteFoldersContent(std::ostream& fout,
-                                   VSFolders const& vsFolders) const;
-
-  virtual void AddSolutionItems(cmLocalGenerator* root,
-                                VSFolders& vsFolders) = 0;
-  virtual void WriteFolderSolutionItems(
-    std::ostream& fout, cmVisualStudioFolder const& folder) const = 0;
 
   bool MarmasmEnabled;
   bool MasmEnabled;

+ 0 - 69
Source/cmGlobalVisualStudio8Generator.cxx

@@ -42,7 +42,6 @@ cmGlobalVisualStudio8Generator::cmGlobalVisualStudio8Generator(
   cmake* cm, std::string const& name)
   : cmGlobalVisualStudio71Generator(cm)
 {
-  this->ProjectConfigurationSectionName = "ProjectConfigurationPlatforms";
   this->Name = name;
   this->ExtraFlagTable =
     cmGlobalVisualStudio8Generator::GetExtraFlagTableVS8();
@@ -356,59 +355,6 @@ void cmGlobalVisualStudio8Generator::AddExtraIDETargets()
   }
 }
 
-void cmGlobalVisualStudio8Generator::WriteSolutionConfigurations(
-  std::ostream& fout, std::vector<std::string> const& configs) const
-{
-  fout << "\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n";
-  for (std::string const& i : configs) {
-    fout << "\t\t" << i << '|' << this->GetPlatformName() << " = " << i << '|'
-         << this->GetPlatformName() << '\n';
-  }
-  fout << "\tEndGlobalSection\n";
-}
-
-void cmGlobalVisualStudio8Generator::WriteProjectConfigurations(
-  std::ostream& fout, std::string const& name, cmGeneratorTarget const& target,
-  std::vector<std::string> const& configs,
-  std::set<std::string> const& configsPartOfDefaultBuild,
-  std::string const& platformMapping) const
-{
-  std::string guid = this->GetGUID(name);
-  for (std::string const& i : configs) {
-    cmList mapConfig;
-    char const* dstConfig = i.c_str();
-    if (target.GetProperty("EXTERNAL_MSPROJECT")) {
-      if (cmValue m = target.GetProperty(
-            cmStrCat("MAP_IMPORTED_CONFIG_", cmSystemTools::UpperCase(i)))) {
-        mapConfig.assign(*m);
-        if (!mapConfig.empty()) {
-          dstConfig = mapConfig[0].c_str();
-        }
-      }
-    }
-    fout << "\t\t{" << guid << "}." << i << '|' << this->GetPlatformName()
-         << ".ActiveCfg = " << dstConfig << '|'
-         << (!platformMapping.empty() ? platformMapping
-                                      : this->GetPlatformName())
-         << '\n';
-    auto ci = configsPartOfDefaultBuild.find(i);
-    if (!(ci == configsPartOfDefaultBuild.end())) {
-      fout << "\t\t{" << guid << "}." << i << '|' << this->GetPlatformName()
-           << ".Build.0 = " << dstConfig << '|'
-           << (!platformMapping.empty() ? platformMapping
-                                        : this->GetPlatformName())
-           << '\n';
-    }
-    if (this->NeedsDeploy(target, dstConfig)) {
-      fout << "\t\t{" << guid << "}." << i << '|' << this->GetPlatformName()
-           << ".Deploy.0 = " << dstConfig << '|'
-           << (!platformMapping.empty() ? platformMapping
-                                        : this->GetPlatformName())
-           << '\n';
-    }
-  }
-}
-
 bool cmGlobalVisualStudio8Generator::NeedsDeploy(
   cmGeneratorTarget const& target, char const* config) const
 {
@@ -444,21 +390,6 @@ bool cmGlobalVisualStudio8Generator::TargetSystemSupportsDeployment() const
   return this->TargetsWindowsCE();
 }
 
-void cmGlobalVisualStudio8Generator::WriteProjectDepends(
-  std::ostream& fout, std::string const&, std::string const&,
-  cmGeneratorTarget const* gt) const
-{
-  TargetDependSet const& unordered = this->GetTargetDirectDepends(gt);
-  OrderedTargetDependSet depends(unordered, std::string());
-  for (cmTargetDepend const& i : depends) {
-    if (!this->IsInSolution(i)) {
-      continue;
-    }
-    std::string guid = this->GetGUID(i->GetName());
-    fout << "\t\t{" << guid << "} = {" << guid << "}\n";
-  }
-}
-
 bool cmGlobalVisualStudio8Generator::NeedLinkLibraryDependencies(
   cmGeneratorTarget* target)
 {

+ 0 - 11
Source/cmGlobalVisualStudio8Generator.h

@@ -70,17 +70,6 @@ protected:
   bool TargetSystemSupportsDeployment() const override;
 
   static cmIDEFlagTable const* GetExtraFlagTableVS8();
-  void WriteSolutionConfigurations(
-    std::ostream& fout,
-    std::vector<std::string> const& configs) const override;
-  void WriteProjectConfigurations(
-    std::ostream& fout, std::string const& name,
-    cmGeneratorTarget const& target, std::vector<std::string> const& configs,
-    std::set<std::string> const& configsPartOfDefaultBuild,
-    std::string const& platformMapping = "") const override;
-  void WriteProjectDepends(std::ostream& fout, std::string const& name,
-                           std::string const& path,
-                           cmGeneratorTarget const* t) const override;
 
   std::string Name;
   std::string WindowsCEVersion;

+ 242 - 57
Source/cmGlobalVisualStudioGenerator.cxx

@@ -123,45 +123,6 @@ char const* cmGlobalVisualStudioGenerator::GetIDEVersion() const
   return "";
 }
 
-void cmGlobalVisualStudioGenerator::WriteSLNHeader(std::ostream& fout) const
-{
-  char utf8bom[] = { char(0xEF), char(0xBB), char(0xBF) };
-  fout.write(utf8bom, 3);
-  fout << '\n';
-
-  switch (this->Version) {
-    case cmGlobalVisualStudioGenerator::VSVersion::VS14:
-      // Visual Studio 14 writes .sln format 12.00
-      fout << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
-      if (this->ExpressEdition) {
-        fout << "# Visual Studio Express 14 for Windows Desktop\n";
-      } else {
-        fout << "# Visual Studio 14\n";
-      }
-      break;
-    case cmGlobalVisualStudioGenerator::VSVersion::VS15:
-      // Visual Studio 15 writes .sln format 12.00
-      fout << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
-      fout << "# Visual Studio 15\n";
-      break;
-    case cmGlobalVisualStudioGenerator::VSVersion::VS16:
-      // Visual Studio 16 writes .sln format 12.00
-      fout << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
-      fout << "# Visual Studio Version 16\n";
-      break;
-    case cmGlobalVisualStudioGenerator::VSVersion::VS17:
-      // Visual Studio 17 writes .sln format 12.00
-      fout << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
-      fout << "# Visual Studio Version 17\n";
-      break;
-    case cmGlobalVisualStudioGenerator::VSVersion::VS18:
-      // Visual Studio 18 writes .sln format 12.00
-      fout << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
-      fout << "# Visual Studio Version 18\n";
-      break;
-  }
-}
-
 std::string cmGlobalVisualStudioGenerator::GetRegistryBase()
 {
   return cmGlobalVisualStudioGenerator::GetRegistryBase(this->GetIDEVersion());
@@ -813,23 +774,8 @@ bool cmGlobalVisualStudioGenerator::Open(std::string const& bindir,
   return std::async(std::launch::async, OpenSolution, sln).get();
 }
 
-std::string cmGlobalVisualStudioGenerator::ConvertToSolutionPath(
-  std::string const& path) const
-{
-  // Convert to backslashes.  Do not use ConvertToOutputPath because
-  // we will add quoting ourselves, and we know these projects always
-  // use windows slashes.
-  std::string d = path;
-  std::string::size_type pos = 0;
-  while ((pos = d.find('/', pos)) != std::string::npos) {
-    d[pos++] = '\\';
-  }
-  return d;
-}
-
 bool cmGlobalVisualStudioGenerator::IsDependedOn(
-  OrderedTargetDependSet const& projectTargets,
-  cmGeneratorTarget const* gtIn) const
+  TargetDependSet const& projectTargets, cmGeneratorTarget const* gtIn) const
 {
   return std::any_of(projectTargets.begin(), projectTargets.end(),
                      [this, gtIn](cmTargetDepend const& l) {
@@ -841,8 +787,7 @@ bool cmGlobalVisualStudioGenerator::IsDependedOn(
 
 std::set<std::string> cmGlobalVisualStudioGenerator::IsPartOfDefaultBuild(
   std::vector<std::string> const& configs,
-  OrderedTargetDependSet const& projectTargets,
-  cmGeneratorTarget const* target) const
+  TargetDependSet const& projectTargets, cmGeneratorTarget const* target) const
 {
   std::set<std::string> activeConfigs;
   // if it is a utility target then only make it part of the
@@ -906,3 +851,243 @@ std::string cmGlobalVisualStudioGenerator::GetGUID(
 
   return cmSystemTools::UpperCase(guid);
 }
+
+cm::VS::Solution::Folder* cmGlobalVisualStudioGenerator::CreateSolutionFolder(
+  cm::VS::Solution& solution, cm::string_view rawName) const
+{
+  cm::VS::Solution::Folder* folder = nullptr;
+  std::string canonicalName;
+  for (std::string::size_type cur = 0;;) {
+    static std::string delims = "/\\";
+    cur = rawName.find_first_not_of(delims, cur);
+    if (cur == std::string::npos) {
+      break;
+    }
+    std::string::size_type end = rawName.find_first_of(delims, cur);
+    cm::string_view f = end == std::string::npos
+      ? rawName.substr(cur)
+      : rawName.substr(cur, end - cur);
+    canonicalName =
+      canonicalName.empty() ? std::string(f) : cmStrCat(canonicalName, '/', f);
+    cm::VS::Solution::Folder* nextFolder = solution.GetFolder(canonicalName);
+    if (nextFolder->Id.empty()) {
+      nextFolder->Id =
+        this->GetGUID(cmStrCat("CMAKE_FOLDER_GUID_"_s, canonicalName));
+      if (folder) {
+        folder->Folders.emplace_back(nextFolder);
+      }
+      solution.Folders.emplace_back(nextFolder);
+    }
+    folder = nextFolder;
+    cur = end;
+  }
+  return folder;
+}
+
+cm::VS::Solution cmGlobalVisualStudioGenerator::CreateSolution(
+  cmLocalGenerator const* root, TargetDependSet const& projectTargets) const
+{
+  using namespace cm::VS;
+  Solution solution;
+  solution.VSVersion = this->Version;
+  solution.VSExpress =
+    this->ExpressEdition ? VersionExpress::Yes : VersionExpress::No;
+  solution.Platform = this->GetPlatformName();
+  solution.Configs =
+    root->GetMakefile()->GetGeneratorConfigs(cmMakefile::ExcludeEmptyConfig);
+  solution.StartupProject = this->GetStartupProjectName(root);
+
+  auto addProject = [this, useFolders = this->UseFolderProperty(),
+                     &solution](cmGeneratorTarget const* gt,
+                                Solution::Project const* p) {
+    if (Solution::Folder* const folder = useFolders
+          ? this->CreateSolutionFolder(solution, gt->GetEffectiveFolderName())
+          : nullptr) {
+      folder->Projects.emplace_back(p);
+    } else {
+      solution.Projects.emplace_back(p);
+    }
+  };
+
+  for (cmTargetDepend const& projectTarget : projectTargets) {
+    cmGeneratorTarget const* gt = projectTarget;
+    if (!this->IsInSolution(gt)) {
+      continue;
+    }
+
+    Solution::Project* project = solution.GetProject(gt->GetName());
+    project->Id = this->GetGUID(gt->GetName());
+
+    std::set<std::string> const& includeConfigs =
+      this->IsPartOfDefaultBuild(solution.Configs, projectTargets, gt);
+    auto addProjectConfig =
+      [this, project, gt, &includeConfigs](std::string const& solutionConfig,
+                                           std::string const& projectConfig) {
+        bool const build =
+          includeConfigs.find(solutionConfig) != includeConfigs.end();
+        bool const deploy = this->NeedsDeploy(*gt, solutionConfig.c_str());
+        project->Configs.emplace_back(
+          Solution::ProjectConfig{ projectConfig, build, deploy });
+      };
+
+    if (cmValue expath = gt->GetProperty("EXTERNAL_MSPROJECT")) {
+      project->Path = *expath;
+      cmValue const projectType = gt->GetProperty("VS_PROJECT_TYPE");
+      if (!projectType.IsEmpty()) {
+        project->TypeId = *projectType;
+      } else {
+        project->TypeId = Solution::Project::TypeIdDefault;
+      }
+      for (std::string const& config : solution.Configs) {
+        cmList mapConfig{ gt->GetProperty(cmStrCat(
+          "MAP_IMPORTED_CONFIG_", cmSystemTools::UpperCase(config))) };
+        addProjectConfig(config, !mapConfig.empty() ? mapConfig[0] : config);
+      }
+      cmValue platformMapping = gt->GetProperty("VS_PLATFORM_MAPPING");
+      project->Platform =
+        !platformMapping.IsEmpty() ? *platformMapping : solution.Platform;
+      for (BT<std::pair<std::string, bool>> const& i : gt->GetUtilities()) {
+        std::string const& dep = i.Value.first;
+        if (this->IsDepInSolution(dep)) {
+          project->BuildDependencies.emplace_back(solution.GetProject(dep));
+        }
+      }
+      addProject(gt, project);
+      continue;
+    }
+
+    cmValue vcprojName = gt->GetProperty("GENERATOR_FILE_NAME");
+    cmValue vcprojType = gt->GetProperty("GENERATOR_FILE_NAME_EXT");
+    if (vcprojName && vcprojType) {
+      cmLocalGenerator* lg = gt->GetLocalGenerator();
+      std::string dir =
+        root->MaybeRelativeToCurBinDir(lg->GetCurrentBinaryDirectory());
+      if (dir == "."_s) {
+        dir.clear();
+      } else if (!cmHasLiteralSuffix(dir, "/")) {
+        dir += "/";
+      }
+
+      project->Path = cmStrCat(dir, *vcprojName, *vcprojType);
+      if (this->TargetIsFortranOnly(gt)) {
+        project->TypeId = Solution::Project::TypeIdFortran;
+      } else if (gt->IsCSharpOnly()) {
+        project->TypeId = Solution::Project::TypeIdCSharp;
+      } else {
+        project->TypeId = Solution::Project::TypeIdDefault;
+      }
+
+      project->Platform =
+        // On VS 19 and above, always map .NET SDK projects to "Any CPU".
+        (gt->IsDotNetSdkTarget() && this->Version >= VSVersion::VS16 &&
+         !cmGlobalVisualStudioGenerator::IsReservedTarget(gt->GetName()))
+        ? "Any CPU"
+        : solution.Platform;
+
+      // Add solution-level dependencies.
+      TargetDependSet const& depends = this->GetTargetDirectDepends(gt);
+      for (cmTargetDepend const& dep : depends) {
+        if (this->IsInSolution(dep)) {
+          project->BuildDependencies.emplace_back(
+            solution.GetProject(dep->GetName()));
+        }
+      }
+
+      for (std::string const& config : solution.Configs) {
+        addProjectConfig(config, config);
+      }
+
+      addProject(gt, project);
+      continue;
+    }
+  }
+
+  cmMakefile* mf = root->GetMakefile();
+  // Unfortunately we have to copy the source groups because
+  // FindSourceGroup uses a regex which is modifying the group.
+  std::vector<cmSourceGroup> sourceGroups = mf->GetSourceGroups();
+  std::vector<std::string> items =
+    cmList{ root->GetMakefile()->GetProperty("VS_SOLUTION_ITEMS") };
+  for (std::string item : items) {
+    if (!cmSystemTools::FileIsFullPath(item)) {
+      item =
+        cmSystemTools::CollapseFullPath(item, mf->GetCurrentSourceDirectory());
+    }
+    cmSourceGroup* sg = mf->FindSourceGroup(item, sourceGroups);
+    std::string folderName = sg->GetFullName();
+    if (folderName.empty()) {
+      folderName = "Solution Items"_s;
+    }
+    Solution::Folder* folder =
+      this->CreateSolutionFolder(solution, folderName);
+    folder->Files.emplace(std::move(item));
+  }
+
+  Solution::PropertyGroup* pgExtensibilityGlobals = nullptr;
+  Solution::PropertyGroup* pgExtensibilityAddIns = nullptr;
+  std::vector<std::string> const propKeys =
+    root->GetMakefile()->GetPropertyKeys();
+  for (std::string const& it : propKeys) {
+    if (!cmHasLiteralPrefix(it, "VS_GLOBAL_SECTION_")) {
+      continue;
+    }
+    std::string name = it.substr(18);
+    Solution::PropertyGroup::Load scope;
+    if (cmHasLiteralPrefix(name, "PRE_")) {
+      name = name.substr(4);
+      scope = Solution::PropertyGroup::Load::Pre;
+    } else if (cmHasLiteralPrefix(name, "POST_")) {
+      name = name.substr(5);
+      scope = Solution::PropertyGroup::Load::Post;
+    } else {
+      continue;
+    }
+    if (name.empty()) {
+      continue;
+    }
+    Solution::PropertyGroup* pg = solution.GetPropertyGroup(name);
+    solution.PropertyGroups.emplace_back(pg);
+    pg->Scope = scope;
+    cmList keyValuePairs{ root->GetMakefile()->GetProperty(it) };
+    for (std::string const& itPair : keyValuePairs) {
+      std::string::size_type const posEqual = itPair.find('=');
+      if (posEqual != std::string::npos) {
+        std::string key = cmTrimWhitespace(itPair.substr(0, posEqual));
+        std::string value = cmTrimWhitespace(itPair.substr(posEqual + 1));
+        pg->Map.emplace(std::move(key), std::move(value));
+      }
+    }
+    if (name == "ExtensibilityGlobals"_s) {
+      pgExtensibilityGlobals = pg;
+    } else if (name == "ExtensibilityAddIns"_s) {
+      pgExtensibilityAddIns = pg;
+    }
+  }
+
+  if (!pgExtensibilityGlobals) {
+    pgExtensibilityGlobals =
+      solution.GetPropertyGroup("ExtensibilityGlobals"_s);
+    solution.PropertyGroups.emplace_back(pgExtensibilityGlobals);
+  }
+  std::string const solutionGuid =
+    this->GetGUID(cmStrCat(root->GetProjectName(), ".sln"));
+  pgExtensibilityGlobals->Map.emplace("SolutionGuid",
+                                      cmStrCat('{', solutionGuid, '}'));
+
+  if (!pgExtensibilityAddIns) {
+    pgExtensibilityAddIns = solution.GetPropertyGroup("ExtensibilityAddIns"_s);
+    solution.PropertyGroups.emplace_back(pgExtensibilityAddIns);
+  }
+
+  solution.CanonicalizeOrder();
+
+  return solution;
+}
+
+void cmGlobalVisualStudioGenerator::WriteSLNFile(
+  std::ostream& fout, cmLocalGenerator* root,
+  TargetDependSet const& projectTargets) const
+{
+  cm::VS::Solution const solution = this->CreateSolution(root, projectTargets);
+  WriteSln(fout, solution);
+}

+ 11 - 6
Source/cmGlobalVisualStudioGenerator.h

@@ -14,6 +14,7 @@
 
 #include "cmGlobalGenerator.h"
 #include "cmTargetDepend.h"
+#include "cmVSSolution.h"
 #include "cmVSVersion.h"
 #include "cmValue.h"
 
@@ -169,16 +170,12 @@ protected:
 
   char const* GetIDEVersion() const;
 
-  void WriteSLNHeader(std::ostream& fout) const;
-
   VSVersion Version;
   bool ExpressEdition;
 
   std::string GeneratorPlatform;
   std::string DefaultPlatformName;
 
-  std::string ConvertToSolutionPath(std::string const& path) const;
-
   /** Return true if the configuration needs to be deployed */
   virtual bool NeedsDeploy(cmGeneratorTarget const& target,
                            char const* config) const = 0;
@@ -188,12 +185,20 @@ protected:
 
   std::set<std::string> IsPartOfDefaultBuild(
     std::vector<std::string> const& configs,
-    OrderedTargetDependSet const& projectTargets,
+    TargetDependSet const& projectTargets,
     cmGeneratorTarget const* target) const;
-  bool IsDependedOn(OrderedTargetDependSet const& projectTargets,
+  bool IsDependedOn(TargetDependSet const& projectTargets,
                     cmGeneratorTarget const* target) const;
   std::map<std::string, std::string> GUIDMap;
 
+  cm::VS::Solution CreateSolution(cmLocalGenerator const* root,
+                                  TargetDependSet const& projectTargets) const;
+  cm::VS::Solution::Folder* CreateSolutionFolder(
+    cm::VS::Solution& solution, cm::string_view rawName) const;
+
+  void WriteSLNFile(std::ostream& fout, cmLocalGenerator* root,
+                    TargetDependSet const& projectTargets) const;
+
 private:
   virtual std::string GetVSMakeProgram() = 0;
   void PrintCompilerAdvice(std::ostream&, std::string const&,

+ 1 - 0
Source/cmGlobalVisualStudioVersionedGenerator.cxx

@@ -8,6 +8,7 @@
 #include <utility>
 #include <vector>
 
+#include <cm/optional>
 #include <cmext/string_view>
 
 #include "cmsys/FStream.hxx"

+ 1 - 0
Source/cmGlobalVisualStudioVersionedGenerator.h

@@ -8,6 +8,7 @@
 #include <string>
 
 #include <cm/optional>
+#include <cm/string_view>
 
 #include "cmGlobalVisualStudio10Generator.h"
 #include "cmGlobalVisualStudio14Generator.h"

+ 283 - 0
Source/cmVSSolution.cxx

@@ -0,0 +1,283 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file LICENSE.rst or https://cmake.org/licensing for details.  */
+#include "cmVSSolution.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include <map>
+
+#include <cm/string_view>
+#include <cmext/string_view>
+
+#include "cmSystemTools.h"
+
+namespace cm {
+namespace VS {
+
+cm::string_view const Solution::Project::TypeIdDefault =
+  "8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942"_s;
+cm::string_view const Solution::Project::TypeIdCSharp =
+  "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"_s;
+cm::string_view const Solution::Project::TypeIdFortran =
+  "6989167D-11E4-40FE-8C1A-2192A86A7E90"_s;
+cm::string_view const Solution::Folder::TypeId =
+  "2150E333-8FDC-42A3-9474-1A3956D46DE8"_s;
+
+std::vector<Solution::Project const*> Solution::GetAllProjects() const
+{
+  std::vector<Project const*> projects;
+  projects.reserve(this->ProjectMap.size());
+  for (Project const* project : this->Projects) {
+    projects.emplace_back(project);
+  }
+  for (Folder const* folder : this->Folders) {
+    for (Project const* project : folder->Projects) {
+      projects.emplace_back(project);
+    }
+  }
+  return projects;
+}
+
+namespace {
+template <typename T>
+T* GetEntry(std::map<cm::string_view, std::unique_ptr<T>>& entryMap,
+            cm::string_view name)
+{
+  auto i = entryMap.find(name);
+  if (i == entryMap.end()) {
+    auto p = cm::make_unique<T>();
+    p->Name = name;
+    i = entryMap.emplace(p->Name, std::move(p)).first;
+  }
+  return i->second.get();
+}
+}
+
+Solution::Folder* Solution::GetFolder(cm::string_view name)
+{
+  return GetEntry(this->FolderMap, name);
+}
+
+Solution::Project* Solution::GetProject(cm::string_view name)
+{
+  return GetEntry(this->ProjectMap, name);
+}
+
+Solution::PropertyGroup* Solution::GetPropertyGroup(cm::string_view name)
+{
+  return GetEntry(this->PropertyGroupMap, name);
+}
+
+namespace {
+struct OrderByName
+{
+  template <typename T>
+  bool operator()(T const* l, T const* r) const
+  {
+    return l->Name < r->Name;
+  }
+};
+}
+
+void Solution::CanonicalizeOrder()
+{
+  std::sort(this->Folders.begin(), this->Folders.end(), OrderByName());
+  for (auto& fi : this->FolderMap) {
+    Folder* folder = fi.second.get();
+    std::sort(folder->Folders.begin(), folder->Folders.end(), OrderByName());
+    std::sort(folder->Projects.begin(), folder->Projects.end(), OrderByName());
+  }
+  std::sort(this->Projects.begin(), this->Projects.end(), OrderByName());
+  for (auto& pi : this->ProjectMap) {
+    Project* project = pi.second.get();
+    std::sort(project->BuildDependencies.begin(),
+              project->BuildDependencies.end(), OrderByName());
+  }
+}
+
+namespace {
+
+void WriteSlnHeader(std::ostream& sln, Version version, VersionExpress express)
+{
+  char utf8bom[] = { char(0xEF), char(0xBB), char(0xBF) };
+  sln.write(utf8bom, 3);
+  sln << '\n';
+
+  switch (version) {
+    case Version::VS14:
+      // Visual Studio 14 writes .sln format 12.00
+      sln << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
+      if (express == VersionExpress::Yes) {
+        sln << "# Visual Studio Express 14 for Windows Desktop\n";
+      } else {
+        sln << "# Visual Studio 14\n";
+      }
+      break;
+    case Version::VS15:
+      // Visual Studio 15 writes .sln format 12.00
+      sln << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
+      sln << "# Visual Studio 15\n";
+      break;
+    case Version::VS16:
+      // Visual Studio 16 writes .sln format 12.00
+      sln << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
+      sln << "# Visual Studio Version 16\n";
+      break;
+    case Version::VS17:
+      // Visual Studio 17 writes .sln format 12.00
+      sln << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
+      sln << "# Visual Studio Version 17\n";
+      break;
+    case Version::VS18:
+      // Visual Studio 18 writes .sln format 12.00
+      sln << "Microsoft Visual Studio Solution File, Format Version 12.00\n";
+      sln << "# Visual Studio Version 18\n";
+      break;
+  }
+}
+
+void WriteSlnProject(std::ostream& sln, Solution::Project const& project)
+{
+  std::string projectPath = project.Path;
+  std::replace(projectPath.begin(), projectPath.end(), '/', '\\');
+  sln << "Project(\"{" << project.TypeId << "}\") = \"" << project.Name
+      << "\", \"" << projectPath << "\", \"{" << project.Id << "}\"\n";
+  sln << "\tProjectSection(ProjectDependencies) = postProject\n";
+  for (Solution::Project const* d : project.BuildDependencies) {
+    sln << "\t\t{" << d->Id << "} = {" << d->Id << "}\n";
+  }
+  sln << "\tEndProjectSection\n";
+  sln << "EndProject\n";
+}
+
+void WriteSlnFolder(std::ostream& sln, Solution::Folder const& folder)
+{
+  std::string folderName = folder.Name;
+  std::replace(folderName.begin(), folderName.end(), '/', '\\');
+  std::string const fileName = cmSystemTools::GetFilenameName(folder.Name);
+  sln << "Project(\"{" << Solution::Folder::TypeId << "}\") = \"" << fileName
+      << "\", \"" << folderName << "\", \"{" << folder.Id << "}\"\n";
+  if (!folder.Files.empty()) {
+    sln << "\tProjectSection(SolutionItems) = preProject\n";
+    for (std::string const& item : folder.Files) {
+      sln << "\t\t" << item << " = " << item << "\n";
+    }
+    sln << "\tEndProjectSection\n";
+  }
+  sln << "EndProject\n";
+}
+
+void WriteSlnSolutionConfigurationPlatforms(std::ostream& sln,
+                                            Solution const& solution)
+{
+  sln << "\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n";
+  for (std::string const& config : solution.Configs) {
+    sln << "\t\t" << config << '|' << solution.Platform << " = " << config
+        << '|' << solution.Platform << '\n';
+  }
+  sln << "\tEndGlobalSection\n";
+}
+
+void WriteSlnProjectConfigurationPlatforms(std::ostream& sln,
+                                           Solution const& solution,
+                                           Solution::Project const& project)
+{
+  auto const writeStep = [&sln, &solution, &project](std::size_t i,
+                                                     cm::string_view step) {
+    sln << "\t\t{" << project.Id << "}." << solution.Configs[i] << '|'
+        << solution.Platform << "." << step << " = "
+        << project.Configs[i].Config << '|' << project.Platform << '\n';
+  };
+  assert(project.Configs.size() == solution.Configs.size());
+  for (std::size_t i = 0; i < solution.Configs.size(); ++i) {
+    writeStep(i, "ActiveCfg"_s);
+    if (project.Configs[i].Build) {
+      writeStep(i, "Build.0"_s);
+    }
+    if (project.Configs[i].Deploy) {
+      writeStep(i, "Deploy.0"_s);
+    }
+  }
+}
+
+void WriteSlnProjectConfigurationPlatforms(
+  std::ostream& sln, Solution const& solution,
+  std::vector<Solution::Project const*> const& projects)
+{
+  sln << "\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n";
+  for (Solution::Project const* project : projects) {
+    WriteSlnProjectConfigurationPlatforms(sln, solution, *project);
+  }
+  sln << "\tEndGlobalSection\n";
+}
+
+void WriteSlnNestedProjects(
+  std::ostream& sln, std::vector<Solution::Folder const*> const& folders)
+{
+  sln << "\tGlobalSection(NestedProjects) = preSolution\n";
+  for (Solution::Folder const* folder : folders) {
+    for (Solution::Folder const* nestedFolder : folder->Folders) {
+      sln << "\t\t{" << nestedFolder->Id << "} = {" << folder->Id << "}\n";
+    }
+    for (Solution::Project const* project : folder->Projects) {
+      sln << "\t\t{" << project->Id << "} = {" << folder->Id << "}\n";
+    }
+  }
+  sln << "\tEndGlobalSection\n";
+}
+
+void WriteSlnPropertyGroup(std::ostream& sln,
+                           Solution::PropertyGroup const& pg)
+{
+  cm::string_view const order = pg.Scope == Solution::PropertyGroup::Load::Pre
+    ? "preSolution"_s
+    : "postSolution"_s;
+  sln << "\tGlobalSection(" << pg.Name << ") = " << order << '\n';
+  for (auto const& i : pg.Map) {
+    sln << "\t\t" << i.first << " = " << i.second << '\n';
+  }
+  sln << "\tEndGlobalSection\n";
+}
+
+}
+
+void WriteSln(std::ostream& sln, Solution const& solution)
+{
+  assert(solution.VSVersion);
+  assert(solution.VSExpress);
+
+  std::vector<Solution::Project const*> projects = solution.GetAllProjects();
+  std::sort(projects.begin(), projects.end(),
+            [&solution](Solution::Project const* l,
+                        Solution::Project const* r) -> bool {
+              if (r->Name == solution.StartupProject) {
+                return false;
+              }
+              if (l->Name == solution.StartupProject) {
+                return true;
+              }
+              return l->Name < r->Name;
+            });
+
+  WriteSlnHeader(sln, *solution.VSVersion, *solution.VSExpress);
+  for (Solution::Folder const* folder : solution.Folders) {
+    WriteSlnFolder(sln, *folder);
+  }
+  for (Solution::Project const* project : projects) {
+    WriteSlnProject(sln, *project);
+  }
+  sln << "Global\n";
+  WriteSlnSolutionConfigurationPlatforms(sln, solution);
+  WriteSlnProjectConfigurationPlatforms(sln, solution, projects);
+  if (!solution.Folders.empty()) {
+    WriteSlnNestedProjects(sln, solution.Folders);
+  }
+  for (Solution::PropertyGroup const* pg : solution.PropertyGroups) {
+    WriteSlnPropertyGroup(sln, *pg);
+  }
+  sln << "EndGlobal\n";
+}
+
+}
+}

+ 169 - 0
Source/cmVSSolution.h

@@ -0,0 +1,169 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file LICENSE.rst or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <cm/memory>
+#include <cm/optional>
+#include <cm/string_view>
+
+#include "cmVSVersion.h"
+
+namespace cm {
+namespace VS {
+
+/** Represent a Visual Studio Solution.
+    In VS terminology, a "project" corresponds to a CMake "target".  */
+struct Solution final
+{
+  Solution() = default;
+  Solution(Solution&&) = default;
+  Solution& operator=(Solution&&) = default;
+
+  /** Represent how a project behaves under one solution config.  */
+  struct ProjectConfig final
+  {
+    /** Project-specific config corresponding to this solution config.
+        This is usually the same as the solution config, but it can map
+        to another config in some cases.  */
+    std::string Config;
+
+    /** Does the project build under this solution config?  */
+    bool Build = false;
+
+    /** Does the project deploy under this solution config?  */
+    bool Deploy = false;
+  };
+
+  /** Represent one project in a Solution.
+      This corresponds to one CMake "target".  */
+  struct Project final
+  {
+    /** Project name.  This corresponds to the CMake "target" name.  */
+    std::string Name;
+
+    /** Project GUID.  */
+    std::string Id;
+
+    /** Project type GUID.  */
+    std::string TypeId;
+
+    /** Path to the project file on disk.
+        This is either absolute or relative to the Solution file.  */
+    std::string Path;
+
+    /** Project-specific platform.  This is usually the same as the
+        Solution::Platform, but it can be different in some cases.  */
+    std::string Platform;
+
+    /** Project-specific configuration corresponding to each solution config.
+        This vector has the same length as the Solution::Configs vector.  */
+    std::vector<ProjectConfig> Configs;
+
+    /** Solution-level dependencies of the project on other projects.  */
+    std::vector<Project const*> BuildDependencies;
+
+    // Project type GUIDs used during creation.
+    static cm::string_view const TypeIdDefault;
+    static cm::string_view const TypeIdCSharp;
+    static cm::string_view const TypeIdFortran;
+  };
+
+  /** Represent one folder in a Solution.  */
+  struct Folder final
+  {
+    /** Canonical folder name.  This includes parent folders separated by
+        forward slashes.  */
+    std::string Name;
+
+    /** Folder GUID.  */
+    std::string Id;
+
+    /** List of folders contained inside this folder.  */
+    std::vector<Folder const*> Folders;
+
+    /** List of projects contained inside this folder.  */
+    std::vector<Project const*> Projects;
+
+    /** Solution-level files contained inside this folder.  */
+    std::set<std::string> Files;
+
+    // Folder type GUID.
+    static cm::string_view const TypeId;
+  };
+
+  /** Represent a group of solution-level Properties.  */
+  struct PropertyGroup final
+  {
+    enum class Load
+    {
+      Pre,
+      Post,
+    };
+
+    /** Properties group name.  */
+    std::string Name;
+
+    /** Properties group load behavior.  */
+    Load Scope = Load::Post;
+
+    /** Property key-value pairs in the group.  */
+    std::map<std::string, std::string> Map;
+  };
+
+  /** Visual Studio major version number, if known.  */
+  cm::optional<Version> VSVersion;
+
+  /** Whether this is a VS Express edition, if known.  */
+  cm::optional<VersionExpress> VSExpress;
+
+  /** Solution-wide target platform.  This is a Windows architecture.  */
+  std::string Platform;
+
+  /** Solution-wide build configurations.
+      This corresponds to CMAKE_CONFIGURATION_TYPES.  */
+  std::vector<std::string> Configs;
+
+  /** List of all folders in the solution.  */
+  std::vector<Folder const*> Folders;
+
+  /** List of projects in the solution that are not in folders.  */
+  std::vector<Project const*> Projects;
+
+  /** List of solution-level property groups.  */
+  std::vector<PropertyGroup const*> PropertyGroups;
+
+  /** Name of the default startup project.  */
+  std::string StartupProject;
+
+  /** Get all projects in the solution, including all folders.  */
+  std::vector<Project const*> GetAllProjects() const;
+
+  // Non-const methods used during creation.
+  Folder* GetFolder(cm::string_view name);
+  Project* GetProject(cm::string_view name);
+  PropertyGroup* GetPropertyGroup(cm::string_view name);
+  void CanonicalizeOrder();
+
+private:
+  Solution(Solution const&) = delete;
+  Solution& operator=(Solution const&) = delete;
+
+  // Own and index named entities.
+  // The string_view keys point at the Name members.
+  std::map<cm::string_view, std::unique_ptr<Folder>> FolderMap;
+  std::map<cm::string_view, std::unique_ptr<Project>> ProjectMap;
+  std::map<cm::string_view, std::unique_ptr<PropertyGroup>> PropertyGroupMap;
+};
+
+/** Write the .sln-format representation.  */
+void WriteSln(std::ostream& sln, Solution const& solution);
+
+}
+}

+ 6 - 0
Source/cmVSVersion.h

@@ -15,5 +15,11 @@ enum class Version : std::uint16_t
   VS17 = 170,
   VS18 = 180,
 };
+
+enum class VersionExpress
+{
+  No,
+  Yes,
+};
 }
 }