diff --git a/.gitignore b/.gitignore index 30c2caeb2..149fa1ffa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Flax stuff *.sln +*.slnx +*.slnx.DotSettings [Dd]ebug/ [Rr]elease/ obj/ @@ -19,6 +21,7 @@ PackagePlatforms_Cert.bat *.suo *.user *.sln.docstates +*.slnx.docstates *.idb *.tlog *.lastbuilddate diff --git a/AGENTS.md b/AGENTS.md index 8cb870037..7b26c1f41 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ Use these commands from the repo root. Generate project files: ```powershell -.\GenerateProjectFiles.bat -vs2022 -log -verbose -printSDKs -dotnet=8 +.\GenerateProjectFiles.bat -vs2026 -log -verbose -printSDKs -dotnet=8 ``` Alternative default generation: @@ -54,7 +54,7 @@ Run the editor: Visual Studio workflow after generation: -- Open `Flax.sln`. +- Open `Flax.slnx`. - Use solution configuration `Editor.Development` and platform `Win64`. - Set `Flax` or `FlaxEngine` as the startup project. diff --git a/README.md b/README.md index b2786f4d6..a6ad50182 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Follow the instructions below to compile and run the engine from source. * Install Git with LFS * Clone repo (with LFS) * Run **GenerateProjectFiles.bat** -* Open `Flax.sln` and set solution configuration to **Editor.Development** and solution platform to **Win64** +* Open `Flax.slnx` and set solution configuration to **Editor.Development** and solution platform to **Win64** * Set Flax (C++) or FlaxEngine (C#) as startup project * Compile Flax project (hit F7 or CTRL+Shift+B) * Optionally set Debug Type to **Managed Only (.NET Core)** to debug C#-only, or **Mixed (.NET Core)** to debug both C++ and C# diff --git a/Source/Editor/Editor.cpp b/Source/Editor/Editor.cpp index f3065c1a1..56f6bd0ef 100644 --- a/Source/Editor/Editor.cpp +++ b/Source/Editor/Editor.cpp @@ -126,6 +126,7 @@ bool Editor::CheckProjectUpgrade() // Remove old project files FileSystem::DeleteFile(root / name + TEXT(".sln")); + FileSystem::DeleteFile(root / name + TEXT(".slnx")); FileSystem::DeleteFile(root / name + TEXT(".csproj")); FileSystem::DeleteFile(root / name + TEXT(".csproj.user")); FileSystem::DeleteFile(root / name + TEXT(".Editor.csproj")); diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp index de6001efd..c8dbba496 100644 --- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp @@ -175,7 +175,7 @@ bool sortInstallations(RiderInstallation* const& i1, RiderInstallation* const& i RiderCodeEditor::RiderCodeEditor(const String& execPath) : _execPath(execPath) - , _solutionPath(Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln")) + , _solutionPath(Globals::ProjectFolder / Editor::Project->Name + TEXT(".slnx")) { } @@ -278,15 +278,29 @@ String RiderCodeEditor::GetName() const String RiderCodeEditor::GetGenerateProjectCustomArgs() const { - return TEXT("-vs2022"); + return TEXT("-vs2026"); +} + +String RiderCodeEditor::GetSolutionPath() const +{ + // Rider prefers .slnx; fall back to .sln if only that exists + String slnxPath = Globals::ProjectFolder / Editor::Project->Name + TEXT(".slnx"); + if (FileSystem::FileExists(slnxPath)) + return slnxPath; + String slnPath = Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln"); + if (FileSystem::FileExists(slnPath)) + return slnPath; + return slnxPath; // Neither exists yet; target .slnx for generation } void RiderCodeEditor::OpenFile(const String& path, int32 line) { // Generate project files if solution is missing - if (!FileSystem::FileExists(_solutionPath)) + String solutionPath = GetSolutionPath(); + if (!FileSystem::FileExists(solutionPath)) { ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); + solutionPath = GetSolutionPath(); } // Open file @@ -295,11 +309,11 @@ void RiderCodeEditor::OpenFile(const String& path, int32 line) #if !PLATFORM_MAC procSettings.FileName = _execPath; - procSettings.Arguments = String::Format(TEXT("\"{0}\" --line {2} \"{1}\""), _solutionPath, path, line); + procSettings.Arguments = String::Format(TEXT("\"{0}\" --line {2} \"{1}\""), solutionPath, path, line); #else // This follows pretty much how all the other engines open rider which deals with cross architecture issues procSettings.FileName = "/usr/bin/open"; - procSettings.Arguments = String::Format(TEXT("-n -a \"{0}\" --args \"{1}\" --line {3} \"{2}\""), _execPath, _solutionPath, path, line); + procSettings.Arguments = String::Format(TEXT("-n -a \"{0}\" --args \"{1}\" --line {3} \"{2}\""), _execPath, solutionPath, path, line); #endif procSettings.HiddenWindow = false; @@ -312,20 +326,22 @@ void RiderCodeEditor::OpenFile(const String& path, int32 line) void RiderCodeEditor::OpenSolution() { // Generate project files if solution is missing - if (!FileSystem::FileExists(_solutionPath)) + String solutionPath = GetSolutionPath(); + if (!FileSystem::FileExists(solutionPath)) { ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); + solutionPath = GetSolutionPath(); } // Open solution CreateProcessSettings procSettings; #if !PLATFORM_MAC procSettings.FileName = _execPath; - procSettings.Arguments = String::Format(TEXT("\"{0}\""), _solutionPath); + procSettings.Arguments = String::Format(TEXT("\"{0}\""), solutionPath); #else // This follows pretty much how all the other engines open rider which deals with cross architecture issues procSettings.FileName = "/usr/bin/open"; - procSettings.Arguments = String::Format(TEXT("-n -a \"{0}\" \"{1}\""), _execPath, _solutionPath); + procSettings.Arguments = String::Format(TEXT("-n -a \"{0}\" \"{1}\""), _execPath, solutionPath); #endif procSettings.HiddenWindow = false; procSettings.WaitForEnd = false; diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.h b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.h index f3c43b5b2..0254ef756 100644 --- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.h +++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.h @@ -14,6 +14,8 @@ private: String _execPath; String _solutionPath; + String GetSolutionPath() const; + public: /// diff --git a/Source/Editor/Scripting/CodeEditors/SystemDefaultCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/SystemDefaultCodeEditor.cpp index 7a4578126..a6fb664a2 100644 --- a/Source/Editor/Scripting/CodeEditors/SystemDefaultCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/SystemDefaultCodeEditor.cpp @@ -2,6 +2,11 @@ #include "SystemDefaultCodeEditor.h" #include "Engine/Platform/CreateProcessSettings.h" +#include "Engine/Platform/FileSystem.h" +#include "Engine/Engine/Globals.h" +#include "Editor/Editor.h" +#include "Editor/ProjectInfo.h" +#include "Editor/Scripting/ScriptsBuilder.h" CodeEditorTypes SystemDefaultCodeEditor::GetType() const { @@ -26,4 +31,24 @@ void SystemDefaultCodeEditor::OpenFile(const String& path, int32 line) void SystemDefaultCodeEditor::OpenSolution() { + String slnxPath = Globals::ProjectFolder / Editor::Project->Name + TEXT(".slnx"); + String slnPath = Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln"); + String solutionPath; + if (FileSystem::FileExists(slnxPath)) + solutionPath = slnxPath; + else if (FileSystem::FileExists(slnPath)) + solutionPath = slnPath; + else + { + ScriptsBuilder::GenerateProject(); + if (FileSystem::FileExists(slnxPath)) + solutionPath = slnxPath; + else if (FileSystem::FileExists(slnPath)) + solutionPath = slnPath; + } + + if (FileSystem::FileExists(solutionPath)) + { + OpenFile(solutionPath, 0); + } } diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioConnection.cpp b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioConnection.cpp index 21c25cfa5..582415331 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioConnection.cpp +++ b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioConnection.cpp @@ -157,6 +157,23 @@ namespace VisualStudio return _wcsicmp(path1, path2) == 0; } + bool AreSolutionPathsEqual(const wchar_t* path1, const wchar_t* path2) + { + // Compare exact paths first (fast path) + if (AreFilePathsEqual(path1, path2)) + return true; + // Strip .sln/.slnx extension and compare stems (handles .sln vs .slnx mismatch) + auto stripSolExt = [](const wchar_t* p, size_t& len) { + if (len >= 5 && _wcsicmp(p + len - 5, L".slnx") == 0) len -= 5; + else if (len >= 4 && _wcsicmp(p + len - 4, L".sln") == 0) len -= 4; + }; + size_t len1 = wcslen(path1); + size_t len2 = wcslen(path2); + stripSolExt(path1, len1); + stripSolExt(path2, len2); + return len1 == len2 && _wcsnicmp(path1, path2, len1) == 0; + } + class ConnectionInternal { public: @@ -226,7 +243,7 @@ namespace VisualStudio if (FAILED(solution->get_FullName(&fullName.Str))) continue; - if (AreFilePathsEqual(connection->SolutionPath, fullName)) + if (AreSolutionPathsEqual(connection->SolutionPath, fullName)) { // Found connection->DTE = dte; diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.cpp b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.cpp index 2d79cc02b..df6bb4a79 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.cpp @@ -49,10 +49,28 @@ VisualStudioEditor::VisualStudioEditor(VisualStudioVersion version, const String default: CRASH; break; } - _solutionPath = Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln"); + // Initialize with the preferred extension; GetSolutionPath() will check both at runtime + const Char* ext = (_version >= VisualStudioVersion::VS2022) ? TEXT(".slnx") : TEXT(".sln"); + _solutionPath = Globals::ProjectFolder / Editor::Project->Name + ext; _solutionPath.Replace('/', '\\'); // Use Windows-style path separators } +String VisualStudioEditor::GetSolutionPath() const +{ + // VS2022+ prefers .slnx; older versions prefer .sln. Fall back to the other if preferred is missing. + const String base = Globals::ProjectFolder / Editor::Project->Name; + const bool preferSlnx = (_version >= VisualStudioVersion::VS2022); + String preferred = base + (preferSlnx ? TEXT(".slnx") : TEXT(".sln")); + preferred.Replace('/', '\\'); + if (FileSystem::FileExists(preferred)) + return preferred; + String fallback = base + (preferSlnx ? TEXT(".sln") : TEXT(".slnx")); + fallback.Replace('/', '\\'); + if (FileSystem::FileExists(fallback)) + return fallback; + return preferred; // Neither exists yet; return preferred so generation targets the right format +} + void VisualStudioEditor::FindEditors(Array* output) { String installDir; @@ -71,22 +89,26 @@ void VisualStudioEditor::FindEditors(Array* output) { auto& info = infos[i]; VisualStudioVersion version; - switch (info.VersionMajor) + if (info.VersionMajor >= 18) { - case 18: version = VisualStudioVersion::VS2026; - break; - case 17: - version = VisualStudioVersion::VS2022; - break; - case 16: - version = VisualStudioVersion::VS2019; - break; - case 15: - version = VisualStudioVersion::VS2017; - break; - default: - break; + } + else + { + switch (info.VersionMajor) + { + case 17: + version = VisualStudioVersion::VS2022; + break; + case 16: + version = VisualStudioVersion::VS2019; + break; + case 15: + version = VisualStudioVersion::VS2017; + break; + default: + continue; + } } String executablePath(info.ExecutablePath); @@ -184,19 +206,23 @@ String VisualStudioEditor::GetName() const String VisualStudioEditor::GetGenerateProjectCustomArgs() const { + if (_version >= VisualStudioVersion::VS2022) + return TEXT("-vs2026"); return String::Format(TEXT("-{0}"), String(ToString(_version)).ToLower()); } void VisualStudioEditor::OpenFile(const String& path, int32 line) { // Generate project files if solution is missing - if (!FileSystem::FileExists(_solutionPath)) + String solutionPath = GetSolutionPath(); + if (!FileSystem::FileExists(solutionPath)) { ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); + solutionPath = GetSolutionPath(); } // Open file - const VisualStudio::Connection connection(*_CLSID, *_solutionPath); + const VisualStudio::Connection connection(*_CLSID, *solutionPath); String tmp = path; tmp.Replace('/', '\\'); // Use Windows-style path separators const auto result = connection.OpenFile(*tmp, line); @@ -209,13 +235,15 @@ void VisualStudioEditor::OpenFile(const String& path, int32 line) void VisualStudioEditor::OpenSolution() { // Generate project files if solution is missing - if (!FileSystem::FileExists(_solutionPath)) + String solutionPath = GetSolutionPath(); + if (!FileSystem::FileExists(solutionPath)) { ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); + solutionPath = GetSolutionPath(); } // Open solution - const VisualStudio::Connection connection(*_CLSID, *_solutionPath); + const VisualStudio::Connection connection(*_CLSID, *solutionPath); const auto result = connection.OpenSolution(); if (result.Failed()) { @@ -227,35 +255,17 @@ void VisualStudioEditor::OnFileAdded(const String& path) { // TODO: finish dynamic files adding to the project - for now just regenerate it ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); - return; - if (!FileSystem::FileExists(_solutionPath)) - { - return; - } - - // Edit solution - const VisualStudio::Connection connection(*_CLSID, *_solutionPath); - if (connection.IsActive()) - { - String tmp = path; - tmp.Replace('/', '\\'); - String tmp2 = tmp.Substring(Globals::ProjectSourceFolder.Length() + 1); - const auto result = connection.AddFile(*tmp, *tmp2); - if (result.Failed()) - { - LOG(Warning, "Cannot add file to project. {0}", String(result.Message.c_str())); - } - } } bool VisualStudioEditor::UseAsyncForOpen() const { // Need to generate project files if missing first - if (!FileSystem::FileExists(_solutionPath)) + String solutionPath = GetSolutionPath(); + if (!FileSystem::FileExists(solutionPath)) return true; // Open in async only when no solution opened - const VisualStudio::Connection connection(*_CLSID, *_solutionPath); + const VisualStudio::Connection connection(*_CLSID, *solutionPath); return !connection.IsActive(); } diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.h b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.h index 5c32a1171..c7ee683dc 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.h +++ b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.h @@ -25,6 +25,8 @@ private: String _CLSID; String _solutionPath; + String GetSolutionPath() const; + public: /// diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp index bf2ef6bb6..e23c1a8e6 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp @@ -130,15 +130,16 @@ String VisualStudioCodeEditor::GetName() const String VisualStudioCodeEditor::GetGenerateProjectCustomArgs() const { - return TEXT("-vs2022 -vscode"); + return TEXT("-vs2026 -vscode"); } void VisualStudioCodeEditor::OpenFile(const String& path, int32 line) { // Generate VS solution files for intellisense - if (!FileSystem::FileExists(Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln"))) + if (!FileSystem::FileExists(Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln")) && + !FileSystem::FileExists(Globals::ProjectFolder / Editor::Project->Name + TEXT(".slnx"))) { - ScriptsBuilder::GenerateProject(TEXT("-vs2022")); + ScriptsBuilder::GenerateProject(TEXT("-vs2026")); } // Generate project files if missing @@ -163,9 +164,10 @@ void VisualStudioCodeEditor::OpenFile(const String& path, int32 line) void VisualStudioCodeEditor::OpenSolution() { // Generate VS solution files for intellisense - if (!FileSystem::FileExists(Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln"))) + if (!FileSystem::FileExists(Globals::ProjectFolder / Editor::Project->Name + TEXT(".sln")) && + !FileSystem::FileExists(Globals::ProjectFolder / Editor::Project->Name + TEXT(".slnx"))) { - ScriptsBuilder::GenerateProject(TEXT("-vs2022")); + ScriptsBuilder::GenerateProject(TEXT("-vs2026")); } // Generate project files if solution is missing diff --git a/Source/Tools/Flax.Build.Tests/TestCommandLine.cs b/Source/Tools/Flax.Build.Tests/TestCommandLine.cs index 0feb788e6..ea6d951a2 100644 --- a/Source/Tools/Flax.Build.Tests/TestCommandLine.cs +++ b/Source/Tools/Flax.Build.Tests/TestCommandLine.cs @@ -190,5 +190,27 @@ namespace Flax.Build.Tests Assert.AreEqual("value1", TestConfig3.Option1[0]); Assert.AreEqual("value2", TestConfig3.Option1[1]); } + + [Test] + public void TestVisualStudioSolutionFileExtension() + { + var gen2026 = (Flax.Build.Projects.VisualStudio.VisualStudioProjectGenerator)Flax.Build.Projects.ProjectGenerator.Create(Flax.Build.Projects.ProjectFormat.VisualStudio2026, TargetType.NativeCpp); + Assert.AreEqual("slnx", gen2026.SolutionFileExtension); + + var genDefault = (Flax.Build.Projects.VisualStudio.VisualStudioProjectGenerator)Flax.Build.Projects.ProjectGenerator.Create(Flax.Build.Projects.ProjectFormat.VisualStudio, TargetType.NativeCpp); + Assert.AreEqual("slnx", genDefault.SolutionFileExtension); + + var gen2022 = (Flax.Build.Projects.VisualStudio.VisualStudioProjectGenerator)Flax.Build.Projects.ProjectGenerator.Create(Flax.Build.Projects.ProjectFormat.VisualStudio2022, TargetType.NativeCpp); + Assert.AreEqual("sln", gen2022.SolutionFileExtension); + + var gen2019 = (Flax.Build.Projects.VisualStudio.VisualStudioProjectGenerator)Flax.Build.Projects.ProjectGenerator.Create(Flax.Build.Projects.ProjectFormat.VisualStudio2019, TargetType.NativeCpp); + Assert.AreEqual("sln", gen2019.SolutionFileExtension); + + var gen2017 = (Flax.Build.Projects.VisualStudio.VisualStudioProjectGenerator)Flax.Build.Projects.ProjectGenerator.Create(Flax.Build.Projects.ProjectFormat.VisualStudio2017, TargetType.NativeCpp); + Assert.AreEqual("sln", gen2017.SolutionFileExtension); + + var gen2015 = (Flax.Build.Projects.VisualStudio.VisualStudioProjectGenerator)Flax.Build.Projects.ProjectGenerator.Create(Flax.Build.Projects.ProjectFormat.VisualStudio2015, TargetType.NativeCpp); + Assert.AreEqual("sln", gen2015.SolutionFileExtension); + } } } diff --git a/Source/Tools/Flax.Build/Build/Builder.Projects.cs b/Source/Tools/Flax.Build/Build/Builder.Projects.cs index 7af540d52..8e3e0dc2a 100644 --- a/Source/Tools/Flax.Build/Build/Builder.Projects.cs +++ b/Source/Tools/Flax.Build/Build/Builder.Projects.cs @@ -207,7 +207,7 @@ namespace Flax.Build if (Configuration.ProjectFormatCLion) projectFormats.Add(ProjectFormat.CMake); if (Configuration.ProjectFormatRider) - projectFormats.Add(ProjectFormat.VisualStudio2022); + projectFormats.Add(ProjectFormat.VisualStudio2026); if (!string.IsNullOrEmpty(Configuration.ProjectFormatCustom)) projectFormats.Add(ProjectFormat.Custom); if (projectFormats.Count == 0) @@ -218,7 +218,7 @@ namespace Flax.Build !projectFormats.Contains(ProjectFormat.VisualStudio2022) && !projectFormats.Contains(ProjectFormat.VisualStudio)) { - projectFormats.Add(ProjectFormat.VisualStudio2022); + projectFormats.Add(ProjectFormat.VisualStudio2026); } foreach (ProjectFormat projectFormat in projectFormats) @@ -692,6 +692,32 @@ namespace Flax.Build { Log.Verbose("Solution -> " + solution.Path); nativeProjectGenerator.GenerateSolution(solution); + + // Clean up obsolete solution file with different extension if present + if (solution.Path.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase)) + { + var oldSln = Path.ChangeExtension(solution.Path, "sln"); + if (File.Exists(oldSln)) + { + Log.Verbose("Removing obsolete solution file " + oldSln); + File.Delete(oldSln); + } + var oldDotSettingsUser = oldSln + ".DotSettings.user"; + if (File.Exists(oldDotSettingsUser)) + File.Delete(oldDotSettingsUser); + } + else if (solution.Path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) + { + var oldSlnx = Path.ChangeExtension(solution.Path, "slnx"); + if (File.Exists(oldSlnx)) + { + Log.Verbose("Removing obsolete solution file " + oldSlnx); + File.Delete(oldSlnx); + } + var oldDotSettingsUser = oldSlnx + ".DotSettings.user"; + if (File.Exists(oldDotSettingsUser)) + File.Delete(oldDotSettingsUser); + } } } } diff --git a/Source/Tools/Flax.Build/Configuration.cs b/Source/Tools/Flax.Build/Configuration.cs index b2787386b..1530c0a9c 100644 --- a/Source/Tools/Flax.Build/Configuration.cs +++ b/Source/Tools/Flax.Build/Configuration.cs @@ -220,9 +220,9 @@ namespace Flax.Build public static bool ProjectFormatCLion = false; /// - /// Generates Visual Studio 2022 project format files for Rider. Valid only with -genproject option. + /// Generates Visual Studio 2026 project format files for Rider. Valid only with -genproject option. /// - [CommandLine("rider", "Generates Visual Studio 2022 project format files for Rider. Valid only with -genproject option.")] + [CommandLine("rider", "Generates Visual Studio 2026 project format files for Rider. Valid only with -genproject option.")] public static bool ProjectFormatRider = false; /// diff --git a/Source/Tools/Flax.Build/Deploy/VCEnvironment.cs b/Source/Tools/Flax.Build/Deploy/VCEnvironment.cs index c67e6ed89..c93277905 100644 --- a/Source/Tools/Flax.Build/Deploy/VCEnvironment.cs +++ b/Source/Tools/Flax.Build/Deploy/VCEnvironment.cs @@ -244,6 +244,8 @@ namespace Flax.Deploy // CMake VS2026 generator prefers .slnx solution files, just swap the extension for CMake dependencies if (File.Exists(Path.ChangeExtension(solutionFile, "slnx"))) solutionFile = Path.ChangeExtension(solutionFile, "slnx"); + else if (File.Exists(Path.ChangeExtension(solutionFile, "sln"))) + solutionFile = Path.ChangeExtension(solutionFile, "sln"); else throw new Exception(string.Format("Unable to build solution {0}. Solution file not found.", solutionFile)); } diff --git a/Source/Tools/Flax.Build/Projects/ProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/ProjectGenerator.cs index 7a52ddc19..c339aed02 100644 --- a/Source/Tools/Flax.Build/Projects/ProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/ProjectGenerator.cs @@ -82,34 +82,10 @@ namespace Flax.Build.Projects /// The generator. public static ProjectGenerator Create(ProjectFormat format, TargetType type) { - // Pick the newest installed Visual Studio version + // Visual Studio default project format is Visual Studio 2026 (.slnx) if (format == ProjectFormat.VisualStudio) { - if (VisualStudioInstance.HasIDE(VisualStudioVersion.VisualStudio2026)) - { - format = ProjectFormat.VisualStudio2026; - } - else if (VisualStudioInstance.HasIDE(VisualStudioVersion.VisualStudio2022)) - { - format = ProjectFormat.VisualStudio2022; - } - else if (VisualStudioInstance.HasIDE(VisualStudioVersion.VisualStudio2019)) - { - format = ProjectFormat.VisualStudio2019; - } - else if (VisualStudioInstance.HasIDE(VisualStudioVersion.VisualStudio2017)) - { - format = ProjectFormat.VisualStudio2017; - } - else if (VisualStudioInstance.HasIDE(VisualStudioVersion.VisualStudio2015)) - { - format = ProjectFormat.VisualStudio2015; - } - else - { - Log.Warning("Failed to find default Visual Studio installation"); - format = ProjectFormat.VisualStudio2015; - } + format = ProjectFormat.VisualStudio2026; } switch (format) diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioInstance.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioInstance.cs index 0fa096089..16591408d 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioInstance.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioInstance.cs @@ -122,17 +122,18 @@ namespace Flax.Build.Projects.VisualStudio { VisualStudioVersion version; string displayName = instance.GetDisplayName(); - if (displayName.Contains("2017")) - version = VisualStudioVersion.VisualStudio2017; - else if (displayName.Contains("2019")) - version = VisualStudioVersion.VisualStudio2019; - else if (displayName.Contains("2022")) - version = VisualStudioVersion.VisualStudio2022; - else if (displayName.Contains("2026")) + string installVer = instance.GetInstallationVersion() ?? string.Empty; + if (displayName.Contains("2026") || installVer.StartsWith("18.")) version = VisualStudioVersion.VisualStudio2026; + else if (displayName.Contains("2022") || installVer.StartsWith("17.")) + version = VisualStudioVersion.VisualStudio2022; + else if (displayName.Contains("2019") || installVer.StartsWith("16.")) + version = VisualStudioVersion.VisualStudio2019; + else if (displayName.Contains("2017") || installVer.StartsWith("15.")) + version = VisualStudioVersion.VisualStudio2017; else { - Log.Warning(string.Format("Unknown Visual Studio installation. Display name: {0}", displayName)); + Log.Warning(string.Format("Unknown Visual Studio installation. Display name: {0}, Version: {1}", displayName, installVer)); continue; } diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs index 86ab6256f..2e8486dba 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs @@ -189,12 +189,55 @@ namespace Flax.Build.Projects.VisualStudio // Hide errors } } + if (File.Exists(path) && Path.GetExtension(path).Equals(".slnx", StringComparison.OrdinalIgnoreCase)) + { + try + { + XmlDocument doc = new XmlDocument(); + doc.Load(path); + XmlNodeList elements = doc.GetElementsByTagName("Project"); + foreach (XmlElement element in elements) + { + var projectPath = element.GetAttribute("Path"); + if (Path.GetFileNameWithoutExtension(projectPath) == projectName && element.HasAttribute("Id")) + return Guid.ParseExact(element.GetAttribute("Id").Trim("{}".ToCharArray()), "D"); + } + } + catch + { + // Hide errors + } + + // Fallback to existing .sln if present + var slnPath = Path.ChangeExtension(path, "sln"); + if (File.Exists(slnPath)) + { + var id = GetProjectGuid(slnPath, projectName); + if (id != Guid.Empty) + return id; + } + } + if (!File.Exists(path)) + { + if (Path.GetExtension(path).Equals(".slnx", StringComparison.OrdinalIgnoreCase)) + { + var slnPath = Path.ChangeExtension(path, "sln"); + if (File.Exists(slnPath)) + return GetProjectGuid(slnPath, projectName); + } + else if (Path.GetExtension(path).Equals(".sln", StringComparison.OrdinalIgnoreCase)) + { + var slnxPath = Path.ChangeExtension(path, "slnx"); + if (File.Exists(slnxPath)) + return GetProjectGuid(slnxPath, projectName); + } + } return Guid.Empty; } /// - public override string SolutionFileExtension => /*Version >= VisualStudioVersion.VisualStudio2026 ? "slnx" :*/ "sln"; + public override string SolutionFileExtension => Version >= VisualStudioVersion.VisualStudio2026 ? "slnx" : "sln"; /// public override Project CreateProject() @@ -282,12 +325,371 @@ namespace Flax.Build.Projects.VisualStudio GenerateXmlSolution(solution); else GenerateAsciiSolution(solution); + + PostGenerateSolution(solution); + } + + private static string GetProjectFolder(VisualStudioProject project) + { + var folder = project.GroupName; + if (project.SourceDirectories != null && project.SourceDirectories.Count == 1) + { + var subFolder = Utilities.NormalizePath(Utilities.MakePathRelativeTo(Path.GetDirectoryName(project.SourceDirectories[0]), project.WorkspaceRootPath)); + if (subFolder.StartsWith("Source/")) + subFolder = subFolder.Substring("Source/".Length); + if (subFolder.Length != 0) + { + if (!string.IsNullOrEmpty(folder)) + folder += '/'; + folder += subFolder; + } + } + return folder != null ? folder.Trim('/') : string.Empty; + } + + private static string EscapeXmlAttribute(string value) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + return value + .Replace("&", "&") + .Replace("\"", """) + .Replace("'", "'") + .Replace("<", "<") + .Replace(">", ">"); + } + + private List GetSolutionConfigurations(Solution solution, VisualStudioProject[] projects) + { + var configurations = new HashSet(); + var mainArchitectures = solution.MainProject?.Targets?.SelectMany(x => x.Architectures).Distinct().ToArray(); + foreach (var project in projects) + { + if (project.Configurations == null || project.Configurations.Count == 0) + throw new Exception("Missing configurations for project " + project.Name); + + // Prevent generating default Debug|AnyCPU and Release|AnyCPU configurations from Flax projects + if (project.Name == "BuildScripts" || project.Name == "Flax.Build" || project.Name == "Flax.Build.Tests") + continue; + + foreach (var configuration in project.Configurations) + { + // Skip architectures which are not included in the game project + if (mainArchitectures != null && !mainArchitectures.Contains(configuration.Architecture)) + continue; + + configurations.Add(new SolutionConfiguration(configuration)); + } + } + + // Add missing configurations (Visual Studio needs all permutations of configuration/platform pair) + var configurationNames = configurations.Select(x => x.Configuration).Distinct().ToArray(); + var platformNames = configurations.Select(x => x.Platform).Distinct().ToArray(); + foreach (var configurationName in configurationNames) + { + foreach (var platformName in platformNames) + { + configurations.Add(new SolutionConfiguration(configurationName, platformName)); + } + } + + // Sort configurations + var configurationsSorted = new List(configurations); + configurationsSorted.Sort(); + return configurationsSorted; } private void GenerateXmlSolution(Solution solution) { - // TODO: Generate the solution file in new format - GenerateAsciiSolution(solution); + var solutionDirectory = Path.GetDirectoryName(solution.Path); + var projects = solution.Projects.Cast().ToArray(); + var configurationsSorted = GetSolutionConfigurations(solution, projects); + + var distinctBuildTypes = configurationsSorted.Select(x => x.Configuration).Distinct().ToList(); + var distinctPlatforms = configurationsSorted.Select(x => x.Platform).Distinct().ToList(); + + var xml = new StringBuilder(); + xml.AppendLine(""); + + // Solution Configurations + xml.AppendLine(" "); + foreach (var buildType in distinctBuildTypes) + xml.AppendLine($" "); + foreach (var platform in distinctPlatforms) + xml.AppendLine($" "); + xml.AppendLine(" "); + + // Solution Folders and Projects + var allFolders = new HashSet(); + var folderProjects = new Dictionary>(); + var rootProjects = new List(); + + foreach (var project in projects) + { + var folder = GetProjectFolder(project); + if (string.IsNullOrEmpty(folder)) + { + rootProjects.Add(project); + } + else + { + if (!folderProjects.TryGetValue(folder, out var list)) + { + list = new List(); + folderProjects.Add(folder, list); + } + list.Add(project); + + // Register folder and all parent folders + var folderParents = folder.Split('/'); + for (int i = 0; i < folderParents.Length; i++) + { + var folderPath = folderParents[0]; + for (int j = 1; j <= i; j++) + folderPath += '/' + folderParents[j]; + allFolders.Add(folderPath); + } + } + } + + var sortedFolders = allFolders.OrderBy(x => x).ToList(); + foreach (var folder in sortedFolders) + { + if (folderProjects.TryGetValue(folder, out var projsInFolder) && projsInFolder.Count > 0) + { + xml.AppendLine($" "); + foreach (var project in projsInFolder) + { + GenerateXmlProject(xml, project, " ", solutionDirectory, configurationsSorted, solution); + } + xml.AppendLine(" "); + } + else + { + xml.AppendLine($" "); + } + } + + foreach (var project in rootProjects) + { + GenerateXmlProject(xml, project, " ", solutionDirectory, configurationsSorted, solution); + } + + xml.AppendLine(""); + + // Save the file + Utilities.WriteFileIfChanged(solution.Path, xml.ToString()); + } + + private void GenerateXmlProject(StringBuilder xml, VisualStudioProject project, string indent, string solutionDirectory, List configurationsSorted, Solution solution) + { + var projectPath = Utilities.NormalizePath(Utilities.MakePathRelativeTo(project.Path, solutionDirectory)); + + string typeAttr = string.Empty; + var ext = Path.GetExtension(project.Path); + bool isStandardVcxproj = ext.Equals(".vcxproj", StringComparison.OrdinalIgnoreCase) && + (project.ProjectTypeGuid == Guid.Empty || project.ProjectTypeGuid == ProjectTypeGuids.WindowsVisualCpp); + bool isStandardCsproj = ext.Equals(".csproj", StringComparison.OrdinalIgnoreCase) && + (project.ProjectTypeGuid == Guid.Empty || project.ProjectTypeGuid == ProjectTypeGuids.WindowsCSharp || project.ProjectTypeGuid == Guid.Parse("9A19103F-16F7-4668-BE54-9A1E7A4F7556")); + if (!isStandardVcxproj && !isStandardCsproj && project.ProjectTypeGuid != Guid.Empty) + { + typeAttr = $" Type=\"{project.ProjectTypeGuid.ToString("D").ToLowerInvariant()}\""; + } + + var childLines = new List(); + + // Build dependencies + if (project.Dependencies.Count > 0) + { + foreach (var dependency in project.Dependencies.Cast()) + { + var depPath = Utilities.NormalizePath(Utilities.MakePathRelativeTo(dependency.Path, solutionDirectory)); + childLines.Add($""); + } + } + + // Per-configuration mappings + var projectMappings = new List<(SolutionConfiguration solConfig, SolutionConfiguration projConfig, bool build)>(); + foreach (var configuration in configurationsSorted) + { + SolutionConfiguration projectConfiguration; + bool build = false; + int firstFullMatch = -1, firstPlatformMatch = -1, firstEditorMatch = -1; + for (int i = 0; i < project.Configurations.Count; i++) + { + var e = new SolutionConfiguration(project.Configurations[i]); + if (e.Name == configuration.Name) + { + firstFullMatch = i; + break; + } + if (firstPlatformMatch == -1 && e.Platform == configuration.Platform) + { + firstPlatformMatch = i; + } + if (firstEditorMatch == -1 && e.Configuration == configuration.Configuration) + { + firstEditorMatch = i; + } + } + if (project is AndroidProject) + { + if (firstFullMatch != -1) + projectConfiguration = new SolutionConfiguration(project.Configurations[firstFullMatch]); + else + projectConfiguration = new SolutionConfiguration(project.Configurations[0]); + } + else if (firstFullMatch != -1) + { + projectConfiguration = new SolutionConfiguration(project.Configurations[firstFullMatch]); + build = solution.MainProject == project; + build |= project.Type == TargetType.DotNetCore; + build |= solution.MainProject == null && project.Name == solution.Name; + } + else if (firstPlatformMatch != -1 && !configuration.Name.StartsWith("Editor.")) + { + projectConfiguration = new SolutionConfiguration(project.Configurations[firstPlatformMatch]); + } + else if (firstEditorMatch != -1 && configuration.Name.StartsWith("Editor.")) + { + projectConfiguration = new SolutionConfiguration(project.Configurations[firstEditorMatch]); + } + else + { + projectConfiguration = new SolutionConfiguration(project.Configurations[0]); + } + + var originalName = projectConfiguration.OriginalName; + var pipeIndex = originalName.IndexOf('|'); + var projConfigName = pipeIndex != -1 ? originalName.Substring(0, pipeIndex) : projectConfiguration.Configuration; + var projPlatformName = pipeIndex != -1 ? originalName.Substring(pipeIndex + 1) : projectConfiguration.Platform; + + projectMappings.Add((configuration, new SolutionConfiguration(projConfigName, projPlatformName), build)); + } + + // BuildType mapping + bool allSameProjConfig = projectMappings.All(m => m.projConfig.Configuration == projectMappings[0].projConfig.Configuration); + if (allSameProjConfig) + { + var commonProjConfig = projectMappings[0].projConfig.Configuration; + if (!projectMappings.All(m => m.solConfig.Configuration == commonProjConfig)) + { + childLines.Add($""); + } + } + else + { + var distinctSolConfigs = projectMappings.Select(m => m.solConfig.Configuration).Distinct().ToList(); + foreach (var solConfigName in distinctSolConfigs) + { + var mappingsForSolConfig = projectMappings.Where(m => m.solConfig.Configuration == solConfigName).ToList(); + bool allPlatformsSame = mappingsForSolConfig.All(m => m.projConfig.Configuration == mappingsForSolConfig[0].projConfig.Configuration); + if (allPlatformsSame) + { + var projConfigName = mappingsForSolConfig[0].projConfig.Configuration; + if (projConfigName != solConfigName) + { + childLines.Add($""); + } + } + else + { + foreach (var m in mappingsForSolConfig) + { + if (m.projConfig.Configuration != m.solConfig.Configuration) + { + childLines.Add($""); + } + } + } + } + } + + // Platform mapping + bool isDotNetProject = project.Type == TargetType.DotNetCore || project.Type == TargetType.DotNet || project.Path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase); + bool allSameProjPlatform = projectMappings.All(m => m.projConfig.Platform == projectMappings[0].projConfig.Platform); + if (allSameProjPlatform) + { + var commonProjPlatform = projectMappings[0].projConfig.Platform; + bool isDefaultDotNetPlatform = isDotNetProject && (commonProjPlatform == "Any CPU" || commonProjPlatform == "AnyCPU"); + bool matchesAllSolPlatforms = projectMappings.All(m => m.solConfig.Platform == commonProjPlatform); + if (!isDefaultDotNetPlatform && !matchesAllSolPlatforms) + { + childLines.Add($""); + } + } + else + { + var distinctSolPlatforms = projectMappings.Select(m => m.solConfig.Platform).Distinct().ToList(); + foreach (var solPlatformName in distinctSolPlatforms) + { + var mappingsForSolPlatform = projectMappings.Where(m => m.solConfig.Platform == solPlatformName).ToList(); + bool allConfigsSame = mappingsForSolPlatform.All(m => m.projConfig.Platform == mappingsForSolPlatform[0].projConfig.Platform); + if (allConfigsSame) + { + var projPlatformName = mappingsForSolPlatform[0].projConfig.Platform; + bool isDefaultDotNetPlatform = isDotNetProject && (projPlatformName == "Any CPU" || projPlatformName == "AnyCPU"); + if (projPlatformName != solPlatformName && !isDefaultDotNetPlatform) + { + childLines.Add($""); + } + } + else + { + foreach (var m in mappingsForSolPlatform) + { + if (m.projConfig.Platform != m.solConfig.Platform) + { + childLines.Add($""); + } + } + } + } + } + + // Build flag mapping + bool allBuildTrue = projectMappings.All(m => m.build); + bool allBuildFalse = projectMappings.All(m => !m.build); + if (allBuildFalse) + { + childLines.Add(""); + } + else if (!allBuildTrue) + { + var distinctSolConfigs = projectMappings.Select(m => m.solConfig.Configuration).Distinct().ToList(); + foreach (var solConfigName in distinctSolConfigs) + { + var mappingsForSolConfig = projectMappings.Where(m => m.solConfig.Configuration == solConfigName).ToList(); + bool allPlatformsFalse = mappingsForSolConfig.All(m => !m.build); + bool allPlatformsTrue = mappingsForSolConfig.All(m => m.build); + if (allPlatformsFalse) + { + childLines.Add($""); + } + else if (!allPlatformsTrue) + { + foreach (var m in mappingsForSolConfig) + { + if (!m.build) + { + childLines.Add($""); + } + } + } + } + } + + if (childLines.Count == 0) + { + xml.AppendLine($"{indent}"); + } + else + { + xml.AppendLine($"{indent}"); + foreach (var line in childLines) + xml.AppendLine($"{indent} {line}"); + xml.AppendLine($"{indent}"); + } } private void GenerateAsciiSolution(Solution solution) @@ -447,42 +849,7 @@ namespace Flax.Build.Projects.VisualStudio { vcSolutionFileContent.AppendLine("Global"); - // Collect all unique configurations - var configurations = new HashSet(); - var mainArchitectures = solution.MainProject?.Targets?.SelectMany(x => x.Architectures).Distinct().ToArray(); - foreach (var project in projects) - { - if (project.Configurations == null || project.Configurations.Count == 0) - throw new Exception("Missing configurations for project " + project.Name); - - // Prevent generating default Debug|AnyCPU and Release|AnyCPU configurations from Flax projects - if (project.Name == "BuildScripts" || project.Name == "Flax.Build" || project.Name == "Flax.Build.Tests") - continue; - - foreach (var configuration in project.Configurations) - { - // Skip architectures which are not included in the game project - if (mainArchitectures != null && !mainArchitectures.Contains(configuration.Architecture)) - continue; - - configurations.Add(new SolutionConfiguration(configuration)); - } - } - - // Add missing configurations (Visual Studio needs all permutations of configuration/platform pair) - var configurationNames = configurations.Select(x => x.Configuration).Distinct().ToArray(); - var platformNames = configurations.Select(x => x.Platform).Distinct().ToArray(); - foreach (var configurationName in configurationNames) - { - foreach (var platformName in platformNames) - { - configurations.Add(new SolutionConfiguration(configurationName, platformName)); - } - } - - // Sort configurations - var configurationsSorted = new List(configurations); - configurationsSorted.Sort(); + var configurationsSorted = GetSolutionConfigurations(solution, projects); // Global configurations { @@ -624,6 +991,12 @@ namespace Flax.Build.Projects.VisualStudio // Save the file Utilities.WriteFileIfChanged(solution.Path, vcSolutionFileContent.ToString()); + } + + private void PostGenerateSolution(Solution solution) + { + var solutionDirectory = Path.GetDirectoryName(solution.Path); + var projects = solution.Projects.Cast().ToArray(); // Generate launch profiles for C# projects if (Version >= VisualStudioVersion.VisualStudio2022) @@ -707,6 +1080,23 @@ namespace Flax.Build.Projects.VisualStudio } Utilities.WriteFileIfChanged(dotSettingsUserFilePath, dotSettingsFileContent.ToString()); + + // Solution settings (team layer) - copy from alternate solution extension if not yet present + string dotSettingsFilePath = solution.Path + ".DotSettings"; + string altDotSettingsFilePath = (solution.Path.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase) + ? Path.ChangeExtension(solution.Path, "sln") + : Path.ChangeExtension(solution.Path, "slnx")) + ".DotSettings"; + if (!File.Exists(dotSettingsFilePath) && File.Exists(altDotSettingsFilePath)) + { + try + { + File.Copy(altDotSettingsFilePath, dotSettingsFilePath, overwrite: true); + } + catch (Exception ex) + { + Log.Warning($"Failed to copy Rider settings from {altDotSettingsFilePath}: {ex.Message}"); + } + } } // Custom MSBuild .targets file to prevent building Flax C#-projects directly with MSBuild diff --git a/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs index d3f5faca4..13f5f6a76 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs @@ -674,7 +674,8 @@ namespace Flax.Build.Projects.VisualStudioCode json.AddField("gulp.autoDetect", "off"); json.AddField("jake.autoDetect", "off"); json.AddField("grunt.autoDetect", "off"); - json.AddField("omnisharp.defaultLaunchSolution", solution.Name + ".sln"); + bool isLegacySln = !Configuration.ProjectFormatVS2026 && (Configuration.ProjectFormatVS2022 || Configuration.ProjectFormatVS2019 || Configuration.ProjectFormatVS2017 || Configuration.ProjectFormatVS2015); + json.AddField("omnisharp.defaultLaunchSolution", solution.Name + (isLegacySln ? ".sln" : ".slnx")); json.AddField("omnisharp.useModernNet", true); json.EndObject();