// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Text; using System.Linq; using System.Collections.Generic; using System.IO; namespace Flax.Build.Projects.VisualStudio { /// /// The Visual Studio project generator for C# projects (.NET SDK .csproj). /// /// public class CSSDKProjectGenerator : VisualStudioProjectGenerator { /// public CSSDKProjectGenerator(VisualStudioVersion version) : base(version) { } /// public override string ProjectFileExtension => "csproj"; /// public override TargetType? Type => TargetType.DotNetCore; /// public override void GenerateProject(Project project, string solutionPath) { var dotnetSdk = DotNetSdk.Instance; var csProjectFileContent = new StringBuilder(); var vsProject = (VisualStudioProject)project; var projectFileToolVersion = ProjectFileToolVersion; var projectDirectory = Path.GetDirectoryName(project.Path); var defaultTarget = project.Targets[0]; foreach (var target in project.Targets) { // Pick the Editor-related target if (target.IsEditor) { defaultTarget = target; break; } } var defaultConfiguration = project.Configurations.First(); foreach (var e in project.Configurations) { if (e.Configuration == defaultConfiguration.Configuration && e.Target == defaultTarget && e.Platform == Platform.BuildTargetPlatform) { defaultConfiguration = e; break; } } // Try to reuse the existing project guid from solution file vsProject.ProjectGuid = GetProjectGuid(solutionPath, vsProject.Name); if (vsProject.ProjectGuid == Guid.Empty) vsProject.ProjectGuid = Guid.NewGuid(); // Header csProjectFileContent.AppendLine(""); csProjectFileContent.AppendLine(""); // Properties csProjectFileContent.AppendLine(" "); // List supported platforms and configurations var allConfigurations = project.Configurations.Select(x => x.Text).Distinct().ToArray(); var allPlatforms = project.Configurations.Select(x => x.ArchitectureName).Distinct().ToArray(); csProjectFileContent.AppendLine(string.Format(" {0}", string.Join(";", allConfigurations))); csProjectFileContent.AppendLine(string.Format(" {0}", string.Join(";", allPlatforms))); // Provide default platform and configuration csProjectFileContent.AppendLine(string.Format(" {0}", defaultConfiguration.Text)); csProjectFileContent.AppendLine(string.Format(" {0}", defaultConfiguration.ArchitectureName)); switch (project.OutputType ?? defaultTarget.OutputType) { case TargetOutputType.Executable: csProjectFileContent.AppendLine(" Exe"); break; case TargetOutputType.Library: csProjectFileContent.AppendLine(" Library"); break; default: throw new ArgumentOutOfRangeException(); } var baseConfiguration = project.Configurations.First(); var baseOutputDir = Utilities.MakePathRelativeTo(project.CSharp.OutputPath ?? baseConfiguration.TargetBuildOptions.OutputFolder, projectDirectory); var baseIntermediateOutputPath = Utilities.MakePathRelativeTo(project.CSharp.IntermediateOutputPath ?? Path.Combine(baseConfiguration.TargetBuildOptions.IntermediateFolder, "CSharp"), projectDirectory); csProjectFileContent.AppendLine($" net{dotnetSdk.Version.Major}.{dotnetSdk.Version.Minor}"); csProjectFileContent.AppendLine(" disable"); csProjectFileContent.AppendLine(string.Format(" {0}", baseConfiguration.TargetBuildOptions.ScriptingAPI.CSharpNullableReferences.ToString().ToLowerInvariant())); csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(string.Format(" {0}", project.BaseName)); csProjectFileContent.AppendLine(string.Format(" {0}.CSharp", project.BaseName)); csProjectFileContent.AppendLine($" {dotnetSdk.CSharpLanguageVersion}"); csProjectFileContent.AppendLine(" 512"); //csProjectFileContent.AppendLine(" false"); // TODO: use it to reduce burden of framework libs // Custom .targets file for overriding MSBuild build tasks, only invoke Flax.Build once per Flax project bool isMainProject = Globals.Project.IsCSharpOnlyProject && Globals.Project.ProjectFolderPath == project.WorkspaceRootPath && project.Name != "BuildScripts" && (Globals.Project.Name != "Flax" || project.Name != "FlaxEngine"); var flaxBuildTargetsFilename = isMainProject ? "Flax.Build.CSharp.targets" : "Flax.Build.CSharp.SkipBuild.targets"; var cacheProjectsPath = Utilities.MakePathRelativeTo(Path.Combine(Globals.Root, "Cache", "Projects"), projectDirectory); var flaxBuildTargetsPath = !string.IsNullOrEmpty(cacheProjectsPath) ? Path.Combine(cacheProjectsPath, flaxBuildTargetsFilename) : flaxBuildTargetsFilename; csProjectFileContent.AppendLine(string.Format(" $(MSBuildThisFileDirectory){0}", flaxBuildTargetsPath)); // Hide annoying warnings during build csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(" True"); csProjectFileContent.AppendLine(" "); csProjectFileContent.AppendLine(""); // Default configuration { WriteConfiguration(project, csProjectFileContent, projectDirectory, defaultConfiguration); } // Configurations foreach (var configuration in project.Configurations) { WriteConfiguration(project, csProjectFileContent, projectDirectory, configuration); } // References csProjectFileContent.AppendLine(" "); /*foreach (var reference in project.CSharp.SystemReferences) { csProjectFileContent.AppendLine(string.Format(" ", reference)); }*/ foreach (var reference in project.CSharp.FileReferences) { csProjectFileContent.AppendLine(string.Format(" ", Path.GetFileNameWithoutExtension(reference))); csProjectFileContent.AppendLine(string.Format(" {0}", Utilities.MakePathRelativeTo(reference, projectDirectory).Replace('/', '\\'))); csProjectFileContent.AppendLine(" "); } foreach (var dependency in project.Dependencies) { csProjectFileContent.AppendLine(string.Format(" ", Utilities.MakePathRelativeTo(dependency.Path, projectDirectory))); csProjectFileContent.AppendLine(string.Format(" {0}", ((VisualStudioProject)dependency).ProjectGuid.ToString("B").ToUpperInvariant())); csProjectFileContent.AppendLine(string.Format(" {0}", dependency.BaseName)); csProjectFileContent.AppendLine(" "); } csProjectFileContent.AppendLine(" "); csProjectFileContent.AppendLine(""); // Files and folders csProjectFileContent.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)); } } foreach (var file in files) { string fileType; if (file.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) fileType = "Compile"; else fileType = "None"; var filePath = file.Replace('/', '\\'); // Normalize path var projectPath = Utilities.MakePathRelativeTo(filePath, projectDirectory); string linkPath = null; if (projectPath.StartsWith(@"..\..\..\")) { // Create folder structure for project external files var sourceIndex = filePath.LastIndexOf(@"\Source\"); if (sourceIndex != -1) { projectPath = filePath; string fileProjectRoot = filePath.Substring(0, sourceIndex); string fileProjectName = Path.GetFileName(fileProjectRoot); string fileProjectRelativePath = filePath.Substring(sourceIndex + 1); // Remove Source-directory from path if (fileProjectRelativePath.IndexOf('\\') != -1) fileProjectRelativePath = fileProjectRelativePath.Substring(fileProjectRelativePath.IndexOf('\\') + 1); if (fileProjectRoot == project.SourceFolderPath) linkPath = fileProjectRelativePath; else // BuildScripts project linkPath = Path.Combine(fileProjectName, fileProjectRelativePath); } } if (!string.IsNullOrEmpty(linkPath)) csProjectFileContent.AppendLine(string.Format(" <{0} Include=\"{1}\" Link=\"{2}\" />", fileType, projectPath, linkPath)); else csProjectFileContent.AppendLine(string.Format(" <{0} Include=\"{1}\" />", fileType, projectPath)); } csProjectFileContent.AppendLine(" "); if (project.GeneratedSourceFiles != null) { foreach (var group in project.GeneratedSourceFiles.GroupBy(x => GetGroupingFromPath(x), y => y)) { (string targetName, string platform, string arch, string configuration) = group.Key; var targetConfiguration = project.Targets.First(x => x.Name == targetName).ConfigurationName; csProjectFileContent.AppendLine($" "); foreach (var file in group) { string fileType; if (file.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) fileType = "Compile"; else fileType = "None"; var filePath = file.Replace('/', '\\'); csProjectFileContent.AppendLine(string.Format(" <{0} Visible=\"false\" Include=\"{1}\" />", fileType, filePath)); } csProjectFileContent.AppendLine(" "); } (string target, string platform, string arch, string configuration) GetGroupingFromPath(string path) { ReadOnlySpan span = path.AsSpan(); Span split = stackalloc Range[path.Count((c) => c == '/' || c == '\\')]; var _ = MemoryExtensions.SplitAny(path, split, [ '/', '\\' ]); return (span[split[^5]].ToString(), span[split[^4]].ToString(), span[split[^3]].ToString(), span[split[^2]].ToString()); } } // End csProjectFileContent.AppendLine(""); if (defaultTarget.CustomExternalProjectFilePath == null) { // Save the files Utilities.WriteFileIfChanged(project.Path, csProjectFileContent.ToString()); } } private void WriteConfiguration(Project project, StringBuilder csProjectFileContent, string projectDirectory, Project.ConfigurationData configuration) { var defines = string.Join(";", project.Defines); if (configuration.TargetBuildOptions.ScriptingAPI.Defines.Count != 0) { if (defines.Length != 0) defines += ";"; defines += string.Join(";", configuration.TargetBuildOptions.ScriptingAPI.Defines); } var outputPath = Utilities.MakePathRelativeTo(project.CSharp.OutputPath ?? configuration.TargetBuildOptions.OutputFolder, projectDirectory); var intermediateOutputPath = Utilities.MakePathRelativeTo(project.CSharp.IntermediateOutputPath ?? Path.Combine(configuration.TargetBuildOptions.IntermediateFolder, "CSharp"), projectDirectory); csProjectFileContent.AppendLine(string.Format(" ", configuration.Name)); csProjectFileContent.AppendLine(" true"); csProjectFileContent.AppendLine(" portable"); csProjectFileContent.AppendLine(string.Format(" {0}", configuration.Configuration == TargetConfiguration.Release ? "true" : "false")); csProjectFileContent.AppendLine(string.Format(" {0}\\", outputPath)); csProjectFileContent.AppendLine(string.Format(" {0}\\", intermediateOutputPath)); csProjectFileContent.AppendLine(string.Format(" {0}\\", intermediateOutputPath)); csProjectFileContent.AppendLine(string.Format(" {0}", defines)); csProjectFileContent.AppendLine(" prompt"); csProjectFileContent.AppendLine(" 4"); csProjectFileContent.AppendLine(" true"); if (configuration.TargetBuildOptions.ScriptingAPI.IgnoreMissingDocumentationWarnings) csProjectFileContent.AppendLine(" 1591"); if (configuration.TargetBuildOptions.ScriptingAPI.IgnoreSpecificWarnings.Any()) { foreach (var warningString in configuration.TargetBuildOptions.ScriptingAPI.IgnoreSpecificWarnings) { csProjectFileContent.AppendLine($" {warningString}"); } } csProjectFileContent.AppendLine(string.Format(" {0}\\{1}.CSharp.xml", outputPath, project.BaseName)); csProjectFileContent.AppendLine(" true"); csProjectFileContent.AppendLine(" "); // Generate configuration-specific references foreach (var reference in configuration.TargetBuildOptions.ScriptingAPI.FileReferences) { if (!project.CSharp.FileReferences.Contains(reference)) { csProjectFileContent.AppendLine(string.Format(" ", configuration.Name)); csProjectFileContent.AppendLine(string.Format(" ", Path.GetFileNameWithoutExtension(reference))); csProjectFileContent.AppendLine(string.Format(" {0}", Utilities.MakePathRelativeTo(reference, projectDirectory).Replace('/', '\\'))); csProjectFileContent.AppendLine(" "); csProjectFileContent.AppendLine(" "); } } foreach (var analyzer in configuration.TargetBuildOptions.ScriptingAPI.Analyzers) { csProjectFileContent.AppendLine(string.Format(" ", configuration.Name)); csProjectFileContent.AppendLine(string.Format(" ", Path.GetFileNameWithoutExtension(analyzer))); csProjectFileContent.AppendLine(string.Format(" {0}", Utilities.MakePathRelativeTo(analyzer, projectDirectory).Replace('/', '\\'))); csProjectFileContent.AppendLine(" "); csProjectFileContent.AppendLine(" "); } csProjectFileContent.AppendLine(""); } } }