// 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 { /// /// The Visual Studio project generator for C++ projects (.vcxproj). /// /// public class VCProjectGenerator : VisualStudioProjectGenerator { /// /// Gets the project file platform toolset version. /// 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; } } /// public VCProjectGenerator(VisualStudioVersion version) : base(version) { } /// public override string ProjectFileExtension => "vcxproj"; /// public override TargetType? Type => TargetType.NativeCpp; private static string FixPath(string path) { if (path.Contains(' ')) { path = "\"" + path + "\""; } return path; } /// 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(""); vcProjectFileContent.AppendLine(string.Format("", projectFileToolVersion)); vcFiltersFileContent.AppendLine(""); vcFiltersFileContent.AppendLine(string.Format("", projectFileToolVersion)); vcUserFileContent.AppendLine(""); vcUserFileContent.AppendLine(string.Format("", projectFileToolVersion)); // Project configurations vcProjectFileContent.AppendLine(" "); foreach (var configuration in project.Configurations) { vcProjectFileContent.AppendLine(string.Format(" ", configuration.Name)); vcProjectFileContent.AppendLine(string.Format(" {0}", configuration.Text)); vcProjectFileContent.AppendLine(string.Format(" {0}", configuration.ArchitectureName)); vcProjectFileContent.AppendLine(" "); } vcProjectFileContent.AppendLine(" "); 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(" "); vcProjectFileContent.AppendLine(string.Format(" {0}", vsProject.ProjectGuid.ToString("B").ToUpperInvariant())); vcProjectFileContent.AppendLine(string.Format(" {0}", project.BaseName)); vcProjectFileContent.AppendLine(string.Format(" {0}", projectFilePlatformToolsetVersion)); vcProjectFileContent.AppendLine(string.Format(" {0}", projectFileToolVersion)); vcProjectFileContent.AppendLine(" Native"); vcProjectFileContent.AppendLine(" Unicode"); vcProjectFileContent.AppendLine(" MakeFileProj"); if (Version >= VisualStudioVersion.VisualStudio2022) vcProjectFileContent.AppendLine(" false"); vcProjectFileContent.AppendLine(" ./"); vcProjectFileContent.AppendLine(" "); // Default properties vcProjectFileContent.AppendLine(" "); // Per configuration options foreach (var configuration in project.Configurations) { vcProjectFileContent.AppendLine(string.Format(" ", configuration.Name)); vcProjectFileContent.AppendLine(" Makefile"); vcProjectFileContent.AppendLine(string.Format(" {0}", projectFilePlatformToolsetVersion)); vcProjectFileContent.AppendLine(" "); } // Other properties vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); // Per configuration property sheets foreach (var configuration in project.Configurations) { vcProjectFileContent.AppendLine(string.Format(" ", configuration.Name)); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); } // User macros vcProjectFileContent.AppendLine(" "); // Per configuration options var buildToolPath = Path.ChangeExtension(Utilities.MakePathRelativeTo(typeof(Builder).Assembly.Location, projectDirectory), null); var preprocessorDefinitions = new HashSet(); var includePaths = new HashSet(); 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(" ", configuration.Name)); if (platform is IVisualStudioProjectCustomizer customizer) customizer.WriteVisualStudioBuildProperties(vsProject, platform, toolchain, configuration, vcProjectFileContent, vcFiltersFileContent, vcUserFileContent); vcProjectFileContent.AppendLine(string.Format(" {0}", targetBuildOptions.IntermediateFolder)); vcProjectFileContent.AppendLine(string.Format(" {0}", targetBuildOptions.OutputFolder)); if (includePaths.Count != 0) vcProjectFileContent.AppendLine(string.Format(" $(IncludePath);{0}", string.Join(";", includePaths))); else vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(string.Format(" {0} -build", cmdLine)); vcProjectFileContent.AppendLine(string.Format(" {0} -rebuild", cmdLine)); vcProjectFileContent.AppendLine(string.Format(" {0} -clean", cmdLine)); vcProjectFileContent.AppendLine(string.Format(" {0}", outputTargetFilePath)); if (preprocessorDefinitions.Count != 0) vcProjectFileContent.AppendLine(string.Format(" $(NMakePreprocessorDefinitions);{0}", string.Join(";", preprocessorDefinitions))); if (includePaths.Count != 0) vcProjectFileContent.AppendLine(string.Format(" $(NMakeIncludeSearchPath);{0}", string.Join(";", includePaths))); var additionalOptions = new List(); additionalOptions.Add("$(AdditionalOptions)"); switch (configuration.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; } vcProjectFileContent.AppendLine(string.Format(" {0}", string.Join(" ", additionalOptions))); vcProjectFileContent.AppendLine(" "); } // Files and folders vcProjectFileContent.AppendLine(" "); vcFiltersFileContent.AppendLine(" "); var files = new List(); 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(); foreach (var file in files) { string fileType; if (file.EndsWith(".h", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".inl", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".hpp", StringComparison.OrdinalIgnoreCase)) { fileType = "ClInclude"; } else if (file.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".cc", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".c", 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 allDirectoriesInPath = new List(); 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(" ", leadingDirectory)); vcFiltersFileContent.AppendLine(string.Format(" {0}", filterGuid)); vcFiltersFileContent.AppendLine(" "); } } } vcFiltersFileContent.AppendLine(string.Format(" <{0} Include=\"{1}\">", fileType, projectPath)); vcFiltersFileContent.AppendLine(string.Format(" {0}", filterPath)); vcFiltersFileContent.AppendLine(string.Format(" ", fileType)); } else { vcFiltersFileContent.AppendLine(string.Format(" <{0} Include=\"{1}\" />", fileType, projectPath)); } } vcProjectFileContent.AppendLine(" "); vcFiltersFileContent.AppendLine(" "); { // IntelliSense information vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(string.Format(" $(NMakePreprocessorDefinitions){0}", (project.Defines.Count > 0 ? (";" + string.Join(";", project.Defines)) : ""))); vcProjectFileContent.AppendLine(string.Format(" $(NMakeIncludeSearchPath){0}", (project.SearchPaths.Length > 0 ? (";" + string.Join(";", project.SearchPaths)) : ""))); vcProjectFileContent.AppendLine(" $(NMakeForcedIncludes)"); vcProjectFileContent.AppendLine(" $(NMakeAssemblySearchPath)"); vcProjectFileContent.AppendLine(" $(NMakeForcedUsingAssemblies)"); vcProjectFileContent.AppendLine(" $(AdditionalOptions)"); vcProjectFileContent.AppendLine(" "); } foreach (var platform in platforms) { if (platform is IVisualStudioProjectCustomizer customizer) customizer.WriteVisualStudioEnd(vsProject, platform, vcProjectFileContent, vcFiltersFileContent, vcUserFileContent); } // End vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(" "); vcProjectFileContent.AppendLine(""); vcFiltersFileContent.AppendLine(""); vcUserFileContent.AppendLine(""); 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(""); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(" $(RootNamespace)$(Configuration.Split('.')[0])"); cppTargetsFileContent.AppendLine(" $(OutDir)/$(TargetName)$(TargetExt)"); cppTargetsFileContent.AppendLine(" "); cppTargetsFileContent.AppendLine(""); 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()); } } }