// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Flax.Build.NativeCpp; namespace Flax.Build.Projects.CMake { /// /// Project generator for CMake-based IDE facade. /// public class CMakeProjectGenerator : ProjectGenerator { /// public override string ProjectFileExtension => "cmake"; /// public override string SolutionFileExtension => "cmake"; /// public override TargetType? Type => null; /// public override void GenerateProject(Project project, string solutionPath, bool isMainProject) { // Not used, solution contains all CMake project definitions. } /// public override void GenerateSolution(Solution solution) { var projectFolder = GetProjectFolder(solution); if (!Directory.Exists(projectFolder)) Directory.CreateDirectory(projectFolder); var generateCLionFiles = Configuration.ProjectFormatCLion; var contents = new StringBuilder(); var buildPresets = new List(); var defaultBuildTarget = string.Empty; var targetNames = new HashSet(); var buildToolPath = Path.ChangeExtension(typeof(Builder).Assembly.Location, null) + Utilities.GetPlatformExecutableExt(); contents.AppendLine("cmake_minimum_required(VERSION 3.20)"); contents.AppendLine(); contents.AppendLine("# Generated by Flax.Build. Do not edit manually."); contents.AppendLine("# CMake is used as an IDE facade. Actual builds are delegated to Flax.Build."); contents.AppendLine($"project({EscapeIdentifier(solution.Name)} LANGUAGES C CXX)"); contents.AppendLine(); contents.AppendLine("set(CMAKE_EXPORT_COMPILE_COMMANDS ON)"); contents.AppendLine($"set(FLAX_WORKSPACE {QuotePath(solution.WorkspaceRootPath, solution.WorkspaceRootPath)})"); contents.AppendLine($"set(FLAX_BUILD_TOOL {QuotePath(buildToolPath, solution.WorkspaceRootPath)})"); if (DotNetSdk.Instance.IsValid) contents.AppendLine($"set(FLAX_DOTNET_ROOT {QuotePath(DotNetSdk.Instance.RootPath, solution.WorkspaceRootPath)})"); contents.AppendLine(); foreach (var project in solution.Projects.Where(x => x.Type == TargetType.NativeCpp)) { var visibleFiles = GetProjectFiles(project, generateCLionFiles); var hasCodeModelConfiguration = TryGetCodeModelConfiguration(project, out var codeModelConfiguration); // Use Flax.Build-selected compile units for the code model. The wider project scan is only for visible files. var compileFiles = hasCodeModelConfiguration ? GetCodeModelCompileFiles(codeModelConfiguration, generateCLionFiles) : visibleFiles.Where(x => IsCompileSourceFile(x, generateCLionFiles)).ToArray(); var files = new HashSet(visibleFiles); files.AddRange(compileFiles); var allFiles = files.OrderBy(x => x).ToArray(); var compileFileSet = new HashSet(compileFiles); var nonCompileFiles = allFiles.Where(x => !compileFileSet.Contains(x)).ToArray(); var includePaths = hasCodeModelConfiguration ? GetIncludePaths(project, codeModelConfiguration) : Array.Empty(); var definitions = hasCodeModelConfiguration ? GetPreprocessorDefinitions(project, codeModelConfiguration) : project.Defines.OrderBy(x => x).ToArray(); var cppStandard = hasCodeModelConfiguration ? GetCppStandard(codeModelConfiguration) : 14; var ideTargetName = GetUniqueTargetName(targetNames, "IDE_" + project.Name); contents.AppendLine($"# {project.Name}"); if (compileFiles.Length != 0) { contents.AppendLine($"add_library({ideTargetName} STATIC EXCLUDE_FROM_ALL)"); contents.AppendLine($"set_target_properties({ideTargetName} PROPERTIES EXCLUDE_FROM_ALL TRUE)"); AppendTargetSources(contents, ideTargetName, allFiles, solution.WorkspaceRootPath); if (nonCompileFiles.Length != 0) { contents.AppendLine("set_source_files_properties("); foreach (var file in nonCompileFiles) contents.AppendLine($" {QuotePath(file, solution.WorkspaceRootPath)}"); contents.AppendLine(" PROPERTIES HEADER_FILE_ONLY TRUE"); contents.AppendLine(")"); } contents.AppendLine($"set_target_properties({ideTargetName} PROPERTIES CXX_STANDARD {cppStandard} CXX_STANDARD_REQUIRED YES)"); AppendTargetPaths(contents, ideTargetName, "target_include_directories", "PRIVATE", includePaths, solution.WorkspaceRootPath); AppendTargetValues(contents, ideTargetName, "target_compile_definitions", "PRIVATE", definitions); } else if (allFiles.Length != 0) { contents.AppendLine($"add_custom_target({ideTargetName} SOURCES"); foreach (var file in allFiles) contents.AppendLine($" {QuotePath(file, solution.WorkspaceRootPath)}"); contents.AppendLine(")"); } foreach (var configuration in project.Configurations) { if (generateCLionFiles && !IsCLionDevelopmentPlatform(configuration.Platform)) continue; var buildTargetName = GetUniqueTargetName(targetNames, "Build_" + project.Name + "_" + configuration.Target.Name + "_" + configuration.PlatformName + "_" + configuration.ConfigurationName + "_" + configuration.ArchitectureName); AppendBuildTarget(contents, buildTargetName, "-build", configuration); var presetSuffix = project.Name + "-" + configuration.Target.Name + "-" + configuration.PlatformName + "-" + configuration.ConfigurationName + "-" + configuration.ArchitectureName; if (IsHostConfiguration(configuration)) AddBuildPreset(buildPresets, "build-" + presetSuffix, "Build " + presetSuffix, buildTargetName); if (defaultBuildTarget.Length == 0 && project == solution.MainProject && configuration.Platform == Platform.BuildPlatform.Target && configuration.Architecture == Platform.BuildTargetArchitecture && configuration.Configuration == TargetConfiguration.Development) defaultBuildTarget = buildTargetName; if (generateCLionFiles) { var runProgram = GetRunProgram(project, configuration, out var runArguments); if (IsHostConfiguration(configuration) && !string.IsNullOrEmpty(runProgram)) { var runTargetName = GetUniqueTargetName(targetNames, "App_" + project.Name + "_" + configuration.Target.Name + "_" + configuration.PlatformName + "_" + configuration.ConfigurationName + "_" + configuration.ArchitectureName); AppendRunTarget(contents, runTargetName, runProgram, runArguments); contents.AppendLine($"add_dependencies({runTargetName} {buildTargetName})"); contents.AppendLine(); } } } contents.AppendLine(); } if (!string.IsNullOrEmpty(defaultBuildTarget)) { contents.AppendLine($"add_custom_target({EscapeIdentifier(solution.Name)}_Build ALL DEPENDS {defaultBuildTarget})"); contents.AppendLine(); } Utilities.WriteFileIfChanged(Path.Combine(projectFolder, "CMakeLists.txt"), contents.ToString()); GeneratePresets(solution, projectFolder, buildPresets); } /// /// Gets the directory that contains the generated CMake facade project. /// public static string GetProjectFolder(Solution solution) { return Path.Combine(solution.WorkspaceRootPath, "Cache", "Projects", "CMake", solution.Name); } private struct CMakeBuildPreset { public string Name; public string DisplayName; public string Target; } private static void AddBuildPreset(List presets, string name, string displayName, string target) { presets.Add(new CMakeBuildPreset { Name = EscapePresetName(name), DisplayName = displayName, Target = target, }); } private static void GeneratePresets(Solution solution, string projectFolder, List buildPresets) { var contents = new StringBuilder(); contents.AppendLine("{"); contents.AppendLine(" \"version\": 3,"); contents.AppendLine(" \"cmakeMinimumRequired\": {"); contents.AppendLine(" \"major\": 3,"); contents.AppendLine(" \"minor\": 20,"); contents.AppendLine(" \"patch\": 0"); contents.AppendLine(" },"); contents.AppendLine(" \"configurePresets\": ["); contents.AppendLine(" {"); contents.AppendLine(" \"name\": \"flax\","); contents.AppendLine(" \"displayName\": \"Flax\","); contents.AppendLine(" \"description\": \"Configure Flax CMake facade for IDE integration.\","); contents.AppendLine(" \"generator\": \"Ninja\","); contents.AppendLine(" \"binaryDir\": \"${sourceDir}/cmake-build-flax\","); contents.AppendLine(" \"cacheVariables\": {"); contents.AppendLine(" \"CMAKE_BUILD_TYPE\": \"Debug\","); contents.AppendLine(" \"CMAKE_EXPORT_COMPILE_COMMANDS\": \"ON\""); contents.AppendLine(" }"); contents.AppendLine(" }"); contents.AppendLine(" ],"); contents.AppendLine(" \"buildPresets\": ["); for (var i = 0; i < buildPresets.Count; i++) { var preset = buildPresets[i]; contents.AppendLine(" {"); contents.AppendLine($" \"name\": {JsonString(preset.Name)},"); contents.AppendLine($" \"displayName\": {JsonString(preset.DisplayName)},"); contents.AppendLine(" \"configurePreset\": \"flax\","); contents.AppendLine($" \"targets\": [{JsonString(preset.Target)}]"); contents.Append(" }"); if (i != buildPresets.Count - 1) contents.Append(','); contents.AppendLine(); } contents.AppendLine(" ]"); contents.AppendLine("}"); Utilities.WriteFileIfChanged(Path.Combine(projectFolder, "CMakePresets.json"), contents.ToString()); } private static void AppendBuildTarget(StringBuilder contents, string targetName, string action, Project.ConfigurationData configuration) { contents.AppendLine($"add_custom_target({targetName}"); contents.Append(" COMMAND "); if (DotNetSdk.Instance.IsValid) contents.Append("${CMAKE_COMMAND} -E env \"DOTNET_ROOT=${FLAX_DOTNET_ROOT}\" "); contents.AppendLine($"\"${{FLAX_BUILD_TOOL}}\" {Quote(action)} \"-log\" \"-mutex\""); contents.AppendLine(" \"-workspace=${FLAX_WORKSPACE}\""); contents.AppendLine($" \"-arch={configuration.ArchitectureName}\""); contents.AppendLine($" \"-configuration={configuration.ConfigurationName}\""); contents.AppendLine($" \"-platform={configuration.PlatformName}\""); contents.AppendLine($" \"-buildTargets={configuration.Target.Name}\""); if (!string.IsNullOrEmpty(Configuration.Compiler)) contents.AppendLine($" \"-compiler={EscapeString(Configuration.Compiler)}\""); if (!string.IsNullOrEmpty(Configuration.Dotnet)) contents.AppendLine($" \"-dotnet={EscapeString(Configuration.Dotnet)}\""); if (Configuration.Sanitizers != Sanitizer.None) contents.AppendLine($" \"-sanitizers={Configuration.Sanitizers}\""); contents.AppendLine(" WORKING_DIRECTORY \"${FLAX_WORKSPACE}\""); contents.AppendLine(" USES_TERMINAL"); contents.AppendLine(" VERBATIM"); contents.AppendLine(")"); contents.AppendLine(); } private static void AppendRunTarget(StringBuilder contents, string targetName, string program, string[] arguments) { var sourcePath = $"${{CMAKE_CURRENT_BINARY_DIR}}/{targetName}.cpp"; contents.AppendLine($"file(WRITE \"{sourcePath}\" [=["); contents.AppendLine("#include "); contents.AppendLine("#include "); contents.AppendLine("#ifdef _WIN32"); contents.AppendLine("#include "); contents.AppendLine("#else"); contents.AppendLine("#include "); contents.AppendLine("#endif"); contents.AppendLine("int main(int argc, char** argv) {"); contents.AppendLine($" const char* program = \"{CppString(program)}\";"); contents.AppendLine(" std::vector args;"); contents.AppendLine(" args.reserve(argc + 1);"); contents.AppendLine(" args.push_back(program);"); foreach (var arg in arguments) contents.AppendLine($" args.push_back(\"{CppString(arg)}\");"); contents.AppendLine(" for (int i = 1; i < argc; i++)"); contents.AppendLine(" args.push_back(argv[i]);"); contents.AppendLine(" args.push_back(nullptr);"); contents.AppendLine("#ifdef _WIN32"); contents.AppendLine(" _execv(args[0], args.data());"); contents.AppendLine("#else"); contents.AppendLine(" execv(args[0], const_cast(args.data()));"); contents.AppendLine("#endif"); contents.AppendLine(" perror(args[0]);"); contents.AppendLine(" return 1;"); contents.AppendLine("}"); contents.AppendLine("]=])"); contents.AppendLine($"add_executable({targetName} EXCLUDE_FROM_ALL \"{sourcePath}\")"); contents.AppendLine($"set_target_properties({targetName} PROPERTIES"); contents.AppendLine(" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_CURRENT_BINARY_DIR}/FlaxLaunchers\""); contents.AppendLine(")"); } private static void AppendTargetSources(StringBuilder contents, string targetName, string[] files, string workspaceRoot) { contents.AppendLine($"target_sources({targetName} PRIVATE"); foreach (var file in files) contents.AppendLine($" {QuotePath(file, workspaceRoot)}"); contents.AppendLine(")"); } private static void AppendTargetPaths(StringBuilder contents, string targetName, string command, string visibility, string[] values, string workspaceRoot) { if (values.Length == 0) return; contents.AppendLine($"{command}({targetName} {visibility}"); foreach (var value in values) contents.AppendLine($" {QuotePath(value, workspaceRoot)}"); contents.AppendLine(")"); } private static void AppendTargetValues(StringBuilder contents, string targetName, string command, string visibility, string[] values) { if (values.Length == 0) return; contents.AppendLine($"{command}({targetName} {visibility}"); foreach (var value in values) contents.AppendLine($" {Quote(value)}"); contents.AppendLine(")"); } private static string[] GetProjectFiles(Project project, bool generateCLionFiles) { var files = new HashSet(); if (project.SourceFiles != null) files.AddRange(project.SourceFiles); if (project.GeneratedSourceFiles != null) files.AddRange(project.GeneratedSourceFiles); if (project.SourceDirectories != null) { foreach (var folder in project.SourceDirectories) { if (Directory.Exists(folder)) files.AddRange(Directory.GetFiles(folder, "*", SearchOption.AllDirectories)); } } files.RemoveWhere(x => !IsProjectFile(x) || Path.GetFileName(x).Equals(".DS_Store", StringComparison.OrdinalIgnoreCase) || (generateCLionFiles && !IsCLionDevelopmentSourceFile(x))); return files.OrderBy(x => x).ToArray(); } private static string[] GetCodeModelCompileFiles(Project.ConfigurationData configuration, bool generateCLionFiles) { var files = new HashSet(); var definitions = new HashSet(configuration.TargetBuildOptions.CompileEnv.PreprocessorDefinitions); AddCompileFiles(files, configuration.TargetBuildOptions.SourceFiles, generateCLionFiles, definitions); if (configuration.Modules != null) { foreach (var module in configuration.Modules) { if (module.Key.BuildNativeCode) AddCompileFiles(files, module.Value.SourceFiles, generateCLionFiles, definitions); } } return files.OrderBy(x => x).ToArray(); } private static void AddCompileFiles(HashSet files, IEnumerable sourceFiles, bool generateCLionFiles, HashSet definitions = null) { if (sourceFiles == null) return; foreach (var file in sourceFiles) { if (IsCompileSourceFile(file, generateCLionFiles, definitions)) files.Add(file); } } private static bool TryGetCodeModelConfiguration(Project project, out Project.ConfigurationData configuration) { foreach (var e in project.Configurations) { if (e.TargetBuildOptions != null && e.Platform == Platform.BuildPlatform.Target && e.Architecture == Platform.BuildTargetArchitecture && e.Configuration == TargetConfiguration.Development) { configuration = e; return true; } } foreach (var e in project.Configurations) { if (e.TargetBuildOptions != null) { configuration = e; return true; } } configuration = default; return false; } private static bool IsHostConfiguration(Project.ConfigurationData configuration) { return configuration.Platform == Platform.BuildPlatform.Target && configuration.Architecture == Platform.BuildTargetArchitecture; } private static bool IsCLionDevelopmentPlatform(TargetPlatform platform) { switch (platform) { case TargetPlatform.Windows: case TargetPlatform.Linux: case TargetPlatform.Mac: return true; default: return false; } } private static string[] GetIncludePaths(Project project, Project.ConfigurationData configuration) { var result = new HashSet(); if (project.SearchPaths != null) result.AddRange(project.SearchPaths); result.AddRange(configuration.TargetBuildOptions.CompileEnv.IncludePaths); return result.OrderBy(x => x).ToArray(); } private static string[] GetPreprocessorDefinitions(Project project, Project.ConfigurationData configuration) { var result = new HashSet(); result.AddRange(project.Defines); result.AddRange(configuration.TargetBuildOptions.CompileEnv.PreprocessorDefinitions); return result.OrderBy(x => x).ToArray(); } private static int GetCppStandard(Project.ConfigurationData configuration) { var result = 14; switch (configuration.TargetBuildOptions.CompileEnv.CppVersion) { case CppVersion.Cpp17: result = Math.Max(result, 17); break; case CppVersion.Cpp20: case CppVersion.Latest: result = Math.Max(result, 20); break; } return result; } private static string GetRunProgram(Project project, Project.ConfigurationData configuration, out string[] arguments) { var target = configuration.Target; var outputType = project.OutputType ?? target.OutputType; if (ShouldRunEditorForProject(configuration, outputType)) { var program = Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), configuration.ConfigurationName, "FlaxEditor" + Utilities.GetPlatformExecutableExt()); var args = new List(); if (configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac) args.Add("-std"); args.Add("-project"); args.Add(project.WorkspaceRootPath); args.Add("-skipCompile"); arguments = args.ToArray(); return program; } arguments = configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac ? new[] { "--std" } : Array.Empty(); return target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); } private static bool ShouldRunEditorForProject(Project.ConfigurationData configuration, TargetOutputType outputType) { return outputType != TargetOutputType.Executable && configuration.Target.IsEditor; } private static bool IsCppCompileFile(string path) { var extension = Path.GetExtension(path); return extension.Equals(".c", StringComparison.OrdinalIgnoreCase) || extension.Equals(".cc", StringComparison.OrdinalIgnoreCase) || extension.Equals(".cpp", StringComparison.OrdinalIgnoreCase) || extension.Equals(".cxx", StringComparison.OrdinalIgnoreCase); } private static bool IsCompileSourceFile(string path, bool generateCLionFiles, HashSet definitions = null) { return IsCppCompileFile(path) && (!generateCLionFiles || (IsCLionDevelopmentSourceFile(path) && IsCLionSourceGuardActive(path, definitions))); } private static bool IsCLionDevelopmentSourceFile(string path) { path = Utilities.NormalizePath(path); return !ContainsPathSegment(path, "Android") && !ContainsPathSegment(path, "AndroidJNI") && !ContainsPathSegment(path, "GDK") && !ContainsPathSegment(path, "iOS") && !ContainsPathSegment(path, "PS4") && !ContainsPathSegment(path, "PS5") && !ContainsPathSegment(path, "Switch") && !ContainsPathSegment(path, "UWP") && !ContainsPathSegment(path, "Web") && !ContainsPathSegment(path, "XboxOne") && !ContainsPathSegment(path, "XboxScarlett") && !Path.GetFileName(path).Contains(".Web.", StringComparison.OrdinalIgnoreCase); } private static bool ContainsPathSegment(string path, string segment) { return path.IndexOf("/" + segment + "/", StringComparison.OrdinalIgnoreCase) != -1; } private static bool IsCLionSourceGuardActive(string path, HashSet definitions) { if (definitions == null) return true; path = Utilities.NormalizePath(path); if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Windows")) return HasDefinition(definitions, "PLATFORM_TOOLS_WINDOWS"); if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Linux")) return HasDefinition(definitions, "PLATFORM_TOOLS_LINUX"); if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Mac")) return HasDefinition(definitions, "PLATFORM_TOOLS_MAC"); if (ContainsSourcePath(path, "Source/Engine/Engine/Windows")) return HasDefinition(definitions, "PLATFORM_WINDOWS") && !HasDefinition(definitions, "USE_EDITOR"); if (ContainsSourcePath(path, "Source/Engine/Engine/Linux")) return HasDefinition(definitions, "PLATFORM_LINUX") && !HasDefinition(definitions, "USE_EDITOR"); if (ContainsSourcePath(path, "Source/Engine/Engine/Mac")) return HasDefinition(definitions, "PLATFORM_MAC") && !HasDefinition(definitions, "USE_EDITOR"); if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Win32")) return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && (HasDefinition(definitions, "PLATFORM_WIN32") || HasDefinition(definitions, "PLATFORM_WINDOWS")); if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Linux")) return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && HasDefinition(definitions, "PLATFORM_LINUX"); if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Mac")) return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && HasDefinition(definitions, "PLATFORM_MAC"); if (ContainsSourcePath(path, "Source/Engine/Platform/Win32")) return HasDefinition(definitions, "PLATFORM_WIN32") || HasDefinition(definitions, "PLATFORM_WINDOWS"); if (ContainsSourcePath(path, "Source/Engine/Platform/Windows")) return HasDefinition(definitions, "PLATFORM_WINDOWS"); if (ContainsSourcePath(path, "Source/Engine/Platform/Linux")) return HasDefinition(definitions, "PLATFORM_LINUX"); if (ContainsSourcePath(path, "Source/Engine/Platform/Mac")) return HasDefinition(definitions, "PLATFORM_MAC"); if (ContainsSourcePath(path, "Source/Engine/Platform/Apple")) return HasDefinition(definitions, "PLATFORM_MAC") || HasDefinition(definitions, "PLATFORM_IOS"); if (ContainsSourcePath(path, "Source/Engine/Platform/Unix")) return HasDefinition(definitions, "PLATFORM_UNIX"); return true; } private static bool ContainsSourcePath(string path, string sourcePath) { return path.IndexOf("/" + sourcePath + "/", StringComparison.OrdinalIgnoreCase) != -1; } private static bool HasDefinition(HashSet definitions, string name) { return definitions.Contains(name) || definitions.Any(x => x.StartsWith(name + "=", StringComparison.Ordinal)); } private static bool IsProjectFile(string path) { var extension = Path.GetExtension(path); return IsCppCompileFile(path) || extension.Equals(".h", StringComparison.OrdinalIgnoreCase) || extension.Equals(".hh", StringComparison.OrdinalIgnoreCase) || extension.Equals(".hpp", StringComparison.OrdinalIgnoreCase) || extension.Equals(".hxx", StringComparison.OrdinalIgnoreCase) || extension.Equals(".inl", StringComparison.OrdinalIgnoreCase) || extension.Equals(".rc", StringComparison.OrdinalIgnoreCase) || extension.Equals(".manifest", StringComparison.OrdinalIgnoreCase) || extension.Equals(".natvis", StringComparison.OrdinalIgnoreCase); } private static string GetUniqueTargetName(HashSet names, string name) { var result = EscapeIdentifier(name); var baseName = result; var index = 2; while (!names.Add(result)) result = baseName + "_" + index++; return result; } private static string EscapeIdentifier(string value) { if (string.IsNullOrEmpty(value)) return "_"; var result = new StringBuilder(value.Length); foreach (var c in value) result.Append(char.IsLetterOrDigit(c) || c == '_' ? c : '_'); if (char.IsDigit(result[0])) result.Insert(0, '_'); return result.ToString(); } private static string EscapePresetName(string value) { var result = new StringBuilder(value.Length); foreach (var c in value) result.Append(char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.' ? char.ToLowerInvariant(c) : '-'); return result.ToString(); } private static string QuotePath(string value, string workspaceRoot, bool includeQuotes = true) { value = Utilities.NormalizePath(value); workspaceRoot = Utilities.NormalizePath(workspaceRoot).TrimEnd('/'); if (value.StartsWith(workspaceRoot + "/", StringComparison.Ordinal)) value = "${FLAX_WORKSPACE}/" + value.Substring(workspaceRoot.Length + 1); return Quote(value, includeQuotes); } private static string Quote(string value, bool includeQuotes = true) { value = EscapeString(value); return includeQuotes ? "\"" + value + "\"" : value; } private static string EscapeString(string value) { return value.Replace("\\", "\\\\").Replace(";", "\\;").Replace("\"", "\\\""); } private static string JsonString(string value) { return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; } private static string CppString(string value) { return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"); } } }