Files
FlaxEngine/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs

428 lines
24 KiB
C#

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using Flax.Build.NativeCpp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Flax.Build.Projects.VisualStudio
{
/// <summary>
/// The Visual Studio project generator for C++ projects (.vcxproj).
/// </summary>
/// <seealso cref="Flax.Build.Projects.VisualStudio.VisualStudioProjectGenerator" />
public class VCProjectGenerator : VisualStudioProjectGenerator
{
/// <summary>
/// Gets the project file platform toolset version.
/// </summary>
public string ProjectFilePlatformToolsetVersion
{
get
{
switch (Version)
{
case VisualStudioVersion.VisualStudio2015: return "v140";
case VisualStudioVersion.VisualStudio2017: return "v141";
case VisualStudioVersion.VisualStudio2019: return "v142";
case VisualStudioVersion.VisualStudio2022: return "v143";
}
return string.Empty;
}
}
/// <inheritdoc />
public VCProjectGenerator(VisualStudioVersion version)
: base(version)
{
}
/// <inheritdoc />
public override string ProjectFileExtension => "vcxproj";
/// <inheritdoc />
public override TargetType? Type => TargetType.NativeCpp;
private static string FixPath(string path)
{
if (path.Contains(' '))
{
path = "\"" + path + "\"";
}
return path;
}
/// <inheritdoc />
public override void GenerateProject(Project project, string solutionPath)
{
var vcProjectFileContent = new StringBuilder();
var vcFiltersFileContent = new StringBuilder();
var vcUserFileContent = new StringBuilder();
var vsProject = (VisualStudioProject)project;
var projectFileToolVersion = ProjectFileToolVersion;
var projectFilePlatformToolsetVersion = ProjectFilePlatformToolsetVersion;
var projectDirectory = Path.GetDirectoryName(project.Path);
var filtersDirectory = project.SourceFolderPath;
// Try to reuse the existing project guid from existing files
vsProject.ProjectGuid = GetProjectGuid(vsProject.Path, vsProject.Name);
if (vsProject.ProjectGuid == Guid.Empty)
vsProject.ProjectGuid = GetProjectGuid(solutionPath, vsProject.Name);
if (vsProject.ProjectGuid == Guid.Empty)
vsProject.ProjectGuid = Guid.NewGuid();
// Header
vcProjectFileContent.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
vcProjectFileContent.AppendLine(string.Format("<Project DefaultTargets=\"Build\" ToolsVersion=\"{0}\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">", projectFileToolVersion));
vcFiltersFileContent.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
vcFiltersFileContent.AppendLine(string.Format("<Project ToolsVersion=\"{0}\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">", projectFileToolVersion));
vcUserFileContent.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
vcUserFileContent.AppendLine(string.Format("<Project ToolsVersion=\"{0}\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">", projectFileToolVersion));
// Project configurations
vcProjectFileContent.AppendLine(" <ItemGroup Label=\"ProjectConfigurations\">");
foreach (var configuration in project.Configurations)
{
vcProjectFileContent.AppendLine(string.Format(" <ProjectConfiguration Include=\"{0}\">", configuration.Name));
vcProjectFileContent.AppendLine(string.Format(" <Configuration>{0}</Configuration>", configuration.Text));
vcProjectFileContent.AppendLine(string.Format(" <Platform>{0}</Platform>", configuration.ArchitectureName));
vcProjectFileContent.AppendLine(" </ProjectConfiguration>");
}
vcProjectFileContent.AppendLine(" </ItemGroup>");
var platforms = vsProject.Configurations.Select(x => Platform.GetPlatform(x.Platform)).Distinct();
foreach (var platform in platforms)
{
if (platform is IVisualStudioProjectCustomizer customizer)
customizer.WriteVisualStudioBegin(vsProject, platform, vcProjectFileContent, vcFiltersFileContent, vcUserFileContent);
}
// Globals
vcProjectFileContent.AppendLine(" <PropertyGroup Label=\"Globals\">");
vcProjectFileContent.AppendLine(string.Format(" <ProjectGuid>{0}</ProjectGuid>", vsProject.ProjectGuid.ToString("B").ToUpperInvariant()));
vcProjectFileContent.AppendLine(string.Format(" <RootNamespace>{0}</RootNamespace>", project.BaseName));
vcProjectFileContent.AppendLine(string.Format(" <PlatformToolset>{0}</PlatformToolset>", projectFilePlatformToolsetVersion));
vcProjectFileContent.AppendLine(string.Format(" <MinimumVisualStudioVersion>{0}</MinimumVisualStudioVersion>", projectFileToolVersion));
vcProjectFileContent.AppendLine(" <TargetRuntime>Native</TargetRuntime>");
vcProjectFileContent.AppendLine(" <CharacterSet>Unicode</CharacterSet>");
vcProjectFileContent.AppendLine(" <Keyword>MakeFileProj</Keyword>");
if (Version >= VisualStudioVersion.VisualStudio2022)
vcProjectFileContent.AppendLine(" <ResolveNuGetPackages>false</ResolveNuGetPackages>");
vcProjectFileContent.AppendLine(" <VCTargetsPath Condition=\"$(Configuration.Contains('Linux'))\">./</VCTargetsPath>");
vcProjectFileContent.AppendLine(" </PropertyGroup>");
// Default properties
vcProjectFileContent.AppendLine(" <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.Default.props\" />");
// Per configuration options
foreach (var configuration in project.Configurations)
{
vcProjectFileContent.AppendLine(string.Format(" <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='{0}'\" Label=\"Configuration\">", configuration.Name));
vcProjectFileContent.AppendLine(" <ConfigurationType>Makefile</ConfigurationType>");
vcProjectFileContent.AppendLine(string.Format(" <PlatformToolset>{0}</PlatformToolset>", projectFilePlatformToolsetVersion));
vcProjectFileContent.AppendLine(" </PropertyGroup>");
}
// Other properties
vcProjectFileContent.AppendLine(" <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.props\" />");
vcProjectFileContent.AppendLine(" <ImportGroup Label=\"ExtensionSettings\" />");
// Per configuration property sheets
foreach (var configuration in project.Configurations)
{
vcProjectFileContent.AppendLine(string.Format(" <ImportGroup Condition=\"'$(Configuration)|$(Platform)'=='{0}'\" Label=\"PropertySheets\">", configuration.Name));
vcProjectFileContent.AppendLine(" <Import Project=\"$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props\" Condition=\"exists('$(UserRootDir)\\Microsoft.Cpp.$(Platform).user.props')\" Label=\"LocalAppDataPlatform\" />");
vcProjectFileContent.AppendLine(" </ImportGroup>");
}
// User macros
vcProjectFileContent.AppendLine(" <PropertyGroup Label=\"UserMacros\" />");
// Per configuration options
var buildToolPath = Path.ChangeExtension(Utilities.MakePathRelativeTo(typeof(Builder).Assembly.Location, projectDirectory), null);
var preprocessorDefinitions = new HashSet<string>();
var includePaths = new HashSet<string>();
foreach (var configuration in project.Configurations)
{
var platform = Platform.GetPlatform(configuration.Platform);
var toolchain = platform.GetToolchain(configuration.Architecture);
var target = configuration.Target;
var targetBuildOptions = configuration.TargetBuildOptions;
preprocessorDefinitions.Clear();
foreach (var e in targetBuildOptions.CompileEnv.PreprocessorDefinitions)
preprocessorDefinitions.Add(e);
includePaths.Clear();
foreach (var e in targetBuildOptions.CompileEnv.IncludePaths)
includePaths.Add(e);
var outputTargetFilePath = target.GetOutputFilePath(targetBuildOptions, project.OutputType);
// Build command for the build tool
var cmdLine = string.Format("{0} -log -mutex -workspace={1} -arch={2} -configuration={3} -platform={4} -buildTargets={5}",
FixPath(buildToolPath),
FixPath(project.WorkspaceRootPath),
configuration.Architecture,
configuration.Configuration,
configuration.Platform,
target.Name);
Configuration.PassArgs(ref cmdLine);
vcProjectFileContent.AppendLine(string.Format(" <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='{0}'\">", configuration.Name));
if (platform is IVisualStudioProjectCustomizer customizer)
customizer.WriteVisualStudioBuildProperties(vsProject, platform, toolchain, configuration, vcProjectFileContent, vcFiltersFileContent, vcUserFileContent);
vcProjectFileContent.AppendLine(string.Format(" <IntDir>{0}</IntDir>", targetBuildOptions.IntermediateFolder));
vcProjectFileContent.AppendLine(string.Format(" <OutDir>{0}</OutDir>", targetBuildOptions.OutputFolder));
if (includePaths.Count != 0)
vcProjectFileContent.AppendLine(string.Format(" <IncludePath>$(IncludePath);{0}</IncludePath>", string.Join(";", includePaths)));
else
vcProjectFileContent.AppendLine(" <IncludePath />");
vcProjectFileContent.AppendLine(" <ReferencePath />");
vcProjectFileContent.AppendLine(" <LibraryPath />");
vcProjectFileContent.AppendLine(" <LibraryWPath />");
vcProjectFileContent.AppendLine(" <SourcePath />");
vcProjectFileContent.AppendLine(" <ExcludePath />");
vcProjectFileContent.AppendLine(string.Format(" <NMakeBuildCommandLine>{0} -build</NMakeBuildCommandLine>", cmdLine));
vcProjectFileContent.AppendLine(string.Format(" <NMakeReBuildCommandLine>{0} -rebuild</NMakeReBuildCommandLine>", cmdLine));
vcProjectFileContent.AppendLine(string.Format(" <NMakeCleanCommandLine>{0} -clean</NMakeCleanCommandLine>", cmdLine));
vcProjectFileContent.AppendLine(string.Format(" <NMakeOutput>{0}</NMakeOutput>", outputTargetFilePath));
if (preprocessorDefinitions.Count != 0)
vcProjectFileContent.AppendLine(string.Format(" <NMakePreprocessorDefinitions>$(NMakePreprocessorDefinitions);{0}</NMakePreprocessorDefinitions>", string.Join(";", preprocessorDefinitions)));
if (includePaths.Count != 0)
vcProjectFileContent.AppendLine(string.Format(" <NMakeIncludeSearchPath>$(NMakeIncludeSearchPath);{0}</NMakeIncludeSearchPath>", string.Join(";", includePaths)));
vcProjectFileContent.AppendLine(" </PropertyGroup>");
}
// Files and folders
vcProjectFileContent.AppendLine(" <ItemGroup>");
vcFiltersFileContent.AppendLine(" <ItemGroup>");
var files = new List<string>();
if (project.SourceFiles != null)
files.AddRange(project.SourceFiles);
if (project.SourceDirectories != null)
{
foreach (var folder in project.SourceDirectories)
{
files.AddRange(Directory.GetFiles(folder, "*", SearchOption.AllDirectories));
}
}
var filterDirectories = new HashSet<string>();
foreach (var file in files)
{
string fileType;
if (file.EndsWith(".h", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".inl", StringComparison.OrdinalIgnoreCase))
{
fileType = "ClInclude";
}
else if (file.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".cc", StringComparison.OrdinalIgnoreCase))
{
fileType = "ClCompile";
}
else if (file.EndsWith(".rc", StringComparison.OrdinalIgnoreCase))
{
fileType = "ResourceCompile";
}
else if (file.EndsWith(".manifest", StringComparison.OrdinalIgnoreCase))
{
fileType = "Manifest";
}
else if (file.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
{
// C# files are added to .csproj
continue;
}
else if (file.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase))
{
// Skip C# project files
continue;
}
else
{
fileType = "None";
}
var projectPath = Utilities.MakePathRelativeTo(file, projectDirectory);
vcProjectFileContent.AppendLine(string.Format(" <{0} Include=\"{1}\"/>", fileType, projectPath));
string filterPath = Utilities.MakePathRelativeTo(file, filtersDirectory);
int lastSeparatorIdx = filterPath.LastIndexOf(Path.DirectorySeparatorChar);
filterPath = lastSeparatorIdx == -1 ? string.Empty : filterPath.Substring(0, lastSeparatorIdx);
if (!string.IsNullOrWhiteSpace(filterPath))
{
// Add directories to the filters for this file
string pathRemaining = filterPath;
if (!filterDirectories.Contains(pathRemaining))
{
List<string> allDirectoriesInPath = new List<string>();
string currentPath = string.Empty;
while (true)
{
if (pathRemaining.Length > 0)
{
int slashIndex = pathRemaining.IndexOf(Path.DirectorySeparatorChar);
string splitDirectory;
if (slashIndex != -1)
{
splitDirectory = pathRemaining.Substring(0, slashIndex);
pathRemaining = pathRemaining.Substring(splitDirectory.Length + 1);
}
else
{
splitDirectory = pathRemaining;
pathRemaining = string.Empty;
}
if (!string.IsNullOrEmpty(currentPath))
{
currentPath += Path.DirectorySeparatorChar;
}
currentPath += splitDirectory;
allDirectoriesInPath.Add(currentPath);
}
else
{
break;
}
}
for (var i = 0; i < allDirectoriesInPath.Count; i++)
{
string leadingDirectory = allDirectoriesInPath[i];
if (!filterDirectories.Contains(leadingDirectory))
{
filterDirectories.Add(leadingDirectory);
string filterGuid = Guid.NewGuid().ToString("B").ToUpperInvariant();
vcFiltersFileContent.AppendLine(string.Format(" <Filter Include=\"{0}\">", leadingDirectory));
vcFiltersFileContent.AppendLine(string.Format(" <UniqueIdentifier>{0}</UniqueIdentifier>", filterGuid));
vcFiltersFileContent.AppendLine(" </Filter>");
}
}
}
vcFiltersFileContent.AppendLine(string.Format(" <{0} Include=\"{1}\">", fileType, projectPath));
vcFiltersFileContent.AppendLine(string.Format(" <Filter>{0}</Filter>", filterPath));
vcFiltersFileContent.AppendLine(string.Format(" </{0}>", fileType));
}
else
{
vcFiltersFileContent.AppendLine(string.Format(" <{0} Include=\"{1}\" />", fileType, projectPath));
}
}
vcProjectFileContent.AppendLine(" </ItemGroup>");
vcFiltersFileContent.AppendLine(" </ItemGroup>");
// IntelliSense information
var additionalDefinitions = project.Defines.ToList();
var additionalOptions = new List<string>();
switch (project.Configurations[0].TargetBuildOptions.CompileEnv.CppVersion)
{
case CppVersion.Cpp14:
additionalOptions.Add("/std:c++14");
break;
case CppVersion.Cpp17:
additionalOptions.Add("/std:c++17");
break;
case CppVersion.Cpp20:
additionalOptions.Add("/std:c++20");
break;
case CppVersion.Latest:
additionalOptions.Add("/std:c++latest");
break;
}
switch (project.Configurations[0].TargetBuildOptions.CompileEnv.Architecture)
{
case CpuArchitecture.Default:
break;
case CpuArchitecture.AVX512:
additionalDefinitions.Add("__AVX512F__");
additionalDefinitions.Add("__AVX512CD__");
additionalDefinitions.Add("__AVX512BW__");
additionalDefinitions.Add("__AVX512DQ__");
additionalDefinitions.Add("__AVX512VL__");
goto case CpuArchitecture.AVX2;
case CpuArchitecture.AVX2:
additionalDefinitions.Add("__AVX2__");
goto case CpuArchitecture.AVX;
case CpuArchitecture.AVX:
additionalDefinitions.Add("__AVX__");
break;
}
vcProjectFileContent.AppendLine(" <PropertyGroup>");
vcProjectFileContent.AppendLine(string.Format(" <NMakePreprocessorDefinitions>$(NMakePreprocessorDefinitions){0}</NMakePreprocessorDefinitions>", (additionalDefinitions.Count > 0 ? (";" + string.Join(";", additionalDefinitions)) : "")));
vcProjectFileContent.AppendLine(string.Format(" <NMakeIncludeSearchPath>$(NMakeIncludeSearchPath){0}</NMakeIncludeSearchPath>", (project.SearchPaths.Length > 0 ? (";" + string.Join(";", project.SearchPaths)) : "")));
vcProjectFileContent.AppendLine(" <NMakeForcedIncludes>$(NMakeForcedIncludes)</NMakeForcedIncludes>");
vcProjectFileContent.AppendLine(" <NMakeAssemblySearchPath>$(NMakeAssemblySearchPath)</NMakeAssemblySearchPath>");
vcProjectFileContent.AppendLine(" <NMakeForcedUsingAssemblies>$(NMakeForcedUsingAssemblies)</NMakeForcedUsingAssemblies>");
vcProjectFileContent.AppendLine(string.Format(" <AdditionalOptions>{0}</AdditionalOptions>", string.Join(" ", additionalOptions)));
vcProjectFileContent.AppendLine(" </PropertyGroup>");
foreach (var platform in platforms)
{
if (platform is IVisualStudioProjectCustomizer customizer)
customizer.WriteVisualStudioEnd(vsProject, platform, vcProjectFileContent, vcFiltersFileContent, vcUserFileContent);
}
// End
vcProjectFileContent.AppendLine(" <ItemDefinitionGroup>");
vcProjectFileContent.AppendLine(" </ItemDefinitionGroup>");
vcProjectFileContent.AppendLine(" <Import Project=\"$(VCTargetsPath)\\Microsoft.Cpp.targets\" />");
vcProjectFileContent.AppendLine(" <ImportGroup Label=\"ExtensionTargets\">");
vcProjectFileContent.AppendLine(" </ImportGroup>");
vcProjectFileContent.AppendLine("</Project>");
vcFiltersFileContent.AppendLine("</Project>");
vcUserFileContent.AppendLine("</Project>");
if (platforms.Any(x => x.Target == TargetPlatform.Linux))
{
// Override MSBuild .targets file with one that runs NMake commands (workaround for Rider on Linux)
var cppTargetsFileContent = new StringBuilder();
cppTargetsFileContent.AppendLine("<Project xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\" TreatAsLocalProperty=\"Platform\">");
cppTargetsFileContent.AppendLine(" <Target Name=\"Build\">");
cppTargetsFileContent.AppendLine(" <Exec Command='$(NMakeBuildCommandLine)'/>");
cppTargetsFileContent.AppendLine(" </Target>");
cppTargetsFileContent.AppendLine(" <Target Name=\"Rebuild\">");
cppTargetsFileContent.AppendLine(" <Exec Command='$(NMakeReBuildCommandLine)'/>");
cppTargetsFileContent.AppendLine(" </Target>");
cppTargetsFileContent.AppendLine(" <Target Name=\"Clean\">");
cppTargetsFileContent.AppendLine(" <Exec Command='$(NMakeCleanCommandLine)'/>");
cppTargetsFileContent.AppendLine(" </Target>");
cppTargetsFileContent.AppendLine(" <PropertyGroup>");
cppTargetsFileContent.AppendLine(" <TargetExt></TargetExt>");
cppTargetsFileContent.AppendLine(" <TargetName>$(RootNamespace)$(Configuration.Split('.')[0])</TargetName>");
cppTargetsFileContent.AppendLine(" <TargetPath>$(OutDir)/$(TargetName)$(TargetExt)</TargetPath>");
cppTargetsFileContent.AppendLine(" </PropertyGroup>");
cppTargetsFileContent.AppendLine("</Project>");
Utilities.WriteFileIfChanged(Path.Combine(projectDirectory, "Microsoft.Cpp.targets"), cppTargetsFileContent.ToString());
Utilities.WriteFileIfChanged(Path.Combine(projectDirectory, "Microsoft.Cpp.Default.props"), vcUserFileContent.ToString());
Utilities.WriteFileIfChanged(Path.Combine(projectDirectory, "Microsoft.Cpp.props"), vcUserFileContent.ToString());
}
// Save the files
Utilities.WriteFileIfChanged(project.Path, vcProjectFileContent.ToString());
Utilities.WriteFileIfChanged(project.Path + ".filters", vcFiltersFileContent.ToString());
Utilities.WriteFileIfChanged(project.Path + ".user", vcUserFileContent.ToString());
}
}
}