Merge branch 'slnx' of https://github.com/Tryibion/FlaxEngine into Tryibion-slnx

This commit is contained in:
2026-10-06 20:24:03 +02:00
19 changed files with 631 additions and 135 deletions
+3
View File
@@ -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
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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#
+1
View File
@@ -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"));
@@ -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;
@@ -14,6 +14,8 @@ private:
String _execPath;
String _solutionPath;
String GetSolutionPath() const;
public:
/// <summary>
@@ -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);
}
}
@@ -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;
@@ -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<CodeEditor*>* output)
{
String installDir;
@@ -71,22 +89,26 @@ void VisualStudioEditor::FindEditors(Array<CodeEditor*>* 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();
}
@@ -25,6 +25,8 @@ private:
String _CLSID;
String _solutionPath;
String GetSolutionPath() const;
public:
/// <summary>
@@ -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
@@ -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);
}
}
}
@@ -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);
}
}
}
}
+2 -2
View File
@@ -220,9 +220,9 @@ namespace Flax.Build
public static bool ProjectFormatCLion = false;
/// <summary>
/// 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.
/// </summary>
[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;
/// <summary>
@@ -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));
}
@@ -82,34 +82,10 @@ namespace Flax.Build.Projects
/// <returns>The generator.</returns>
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)
@@ -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;
}
@@ -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;
}
/// <inheritdoc />
public override string SolutionFileExtension => /*Version >= VisualStudioVersion.VisualStudio2026 ? "slnx" :*/ "sln";
public override string SolutionFileExtension => Version >= VisualStudioVersion.VisualStudio2026 ? "slnx" : "sln";
/// <inheritdoc />
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("&", "&amp;")
.Replace("\"", "&quot;")
.Replace("'", "&apos;")
.Replace("<", "&lt;")
.Replace(">", "&gt;");
}
private List<SolutionConfiguration> GetSolutionConfigurations(Solution solution, VisualStudioProject[] projects)
{
var configurations = new HashSet<SolutionConfiguration>();
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<SolutionConfiguration>(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<VisualStudioProject>().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>");
// Solution Configurations
xml.AppendLine(" <Configurations>");
foreach (var buildType in distinctBuildTypes)
xml.AppendLine($" <BuildType Name=\"{EscapeXmlAttribute(buildType)}\" />");
foreach (var platform in distinctPlatforms)
xml.AppendLine($" <Platform Name=\"{EscapeXmlAttribute(platform)}\" />");
xml.AppendLine(" </Configurations>");
// Solution Folders and Projects
var allFolders = new HashSet<string>();
var folderProjects = new Dictionary<string, List<VisualStudioProject>>();
var rootProjects = new List<VisualStudioProject>();
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<VisualStudioProject>();
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($" <Folder Name=\"/{EscapeXmlAttribute(folder)}/\">");
foreach (var project in projsInFolder)
{
GenerateXmlProject(xml, project, " ", solutionDirectory, configurationsSorted, solution);
}
xml.AppendLine(" </Folder>");
}
else
{
xml.AppendLine($" <Folder Name=\"/{EscapeXmlAttribute(folder)}/\" />");
}
}
foreach (var project in rootProjects)
{
GenerateXmlProject(xml, project, " ", solutionDirectory, configurationsSorted, solution);
}
xml.AppendLine("</Solution>");
// Save the file
Utilities.WriteFileIfChanged(solution.Path, xml.ToString());
}
private void GenerateXmlProject(StringBuilder xml, VisualStudioProject project, string indent, string solutionDirectory, List<SolutionConfiguration> 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<string>();
// Build dependencies
if (project.Dependencies.Count > 0)
{
foreach (var dependency in project.Dependencies.Cast<VisualStudioProject>())
{
var depPath = Utilities.NormalizePath(Utilities.MakePathRelativeTo(dependency.Path, solutionDirectory));
childLines.Add($"<BuildDependency Project=\"{EscapeXmlAttribute(depPath)}\" />");
}
}
// 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($"<BuildType Project=\"{EscapeXmlAttribute(commonProjConfig)}\" />");
}
}
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($"<BuildType Solution=\"{EscapeXmlAttribute(solConfigName)}|*\" Project=\"{EscapeXmlAttribute(projConfigName)}\" />");
}
}
else
{
foreach (var m in mappingsForSolConfig)
{
if (m.projConfig.Configuration != m.solConfig.Configuration)
{
childLines.Add($"<BuildType Solution=\"{EscapeXmlAttribute(m.solConfig.Name)}\" Project=\"{EscapeXmlAttribute(m.projConfig.Configuration)}\" />");
}
}
}
}
}
// 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($"<Platform Project=\"{EscapeXmlAttribute(commonProjPlatform)}\" />");
}
}
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($"<Platform Solution=\"*|{EscapeXmlAttribute(solPlatformName)}\" Project=\"{EscapeXmlAttribute(projPlatformName)}\" />");
}
}
else
{
foreach (var m in mappingsForSolPlatform)
{
if (m.projConfig.Platform != m.solConfig.Platform)
{
childLines.Add($"<Platform Solution=\"{EscapeXmlAttribute(m.solConfig.Name)}\" Project=\"{EscapeXmlAttribute(m.projConfig.Platform)}\" />");
}
}
}
}
}
// Build flag mapping
bool allBuildTrue = projectMappings.All(m => m.build);
bool allBuildFalse = projectMappings.All(m => !m.build);
if (allBuildFalse)
{
childLines.Add("<Build Project=\"false\" />");
}
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($"<Build Solution=\"{EscapeXmlAttribute(solConfigName)}|*\" Project=\"false\" />");
}
else if (!allPlatformsTrue)
{
foreach (var m in mappingsForSolConfig)
{
if (!m.build)
{
childLines.Add($"<Build Solution=\"{EscapeXmlAttribute(m.solConfig.Name)}\" Project=\"false\" />");
}
}
}
}
}
if (childLines.Count == 0)
{
xml.AppendLine($"{indent}<Project Path=\"{EscapeXmlAttribute(projectPath)}\"{typeAttr} />");
}
else
{
xml.AppendLine($"{indent}<Project Path=\"{EscapeXmlAttribute(projectPath)}\"{typeAttr}>");
foreach (var line in childLines)
xml.AppendLine($"{indent} {line}");
xml.AppendLine($"{indent}</Project>");
}
}
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<SolutionConfiguration>();
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<SolutionConfiguration>(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<VisualStudioProject>().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
@@ -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();