// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { class VSCodeProject : ProjectFile { public VSCodeProject(FileReference InitFilePath, DirectoryReference BaseDir) : base(InitFilePath, BaseDir) { } public override bool WriteProjectFile(List InPlatforms, List InConfigurations, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger) { return true; } } class VSCodeProjectFileGenerator : ProjectFileGenerator { private UnrealTargetPlatform HostPlatform = BuildHostPlatform.Current.Platform; private bool bForeignProject; private DirectoryReference ProjectRoot; private string FrameworkExecutableExtension = OperatingSystem.IsWindows() ? ".exe" : ""; private string FrameworkLibraryExtension = ".dll"; private readonly ConcurrentBag BuildTargets = new(); /// /// Includes all files in the generated workspace. /// [XmlConfigFile(Name = "IncludeAllFiles")] private bool IncludeAllFiles = false; /// /// Whether VS Code project generation should include debug configurations to allow attaching to already running processes /// [XmlConfigFile(Name = "AddDebugAttachConfig")] private bool bAddDebugAttachConfig = false; /// /// Whether VS Code project generation should include debug configurations to allow core dump debugging /// [XmlConfigFile(Name = "AddDebugCoreConfig")] private bool bAddDebugCoreConfig = false; /// /// Do not create compile commands json files with compiler arguments for each file; works better with VS Code extension using /// UBT server mode. /// [XmlConfigFile(Name = "NoCompileCommands")] [CommandLine("-NoCompileCommands")] private bool bNoCompileCommands = false; /// /// Create a workspace file for use with VS Code extension that communicates directly with UBT. /// [XmlConfigFile(Name = "UseVSCodeExtension")] [CommandLine("-UseVSCodeExtension")] private bool bUseVSCodeExtension = false; private enum EPathType { Absolute, Relative, } private enum EQuoteType { Single, // can be ignored on platforms that don't need it (windows atm) Double, } private string CommonMakePathString(FileSystemReference InRef, EPathType InPathType, DirectoryReference? InRelativeRoot) { if (InRelativeRoot == null) { InRelativeRoot = ProjectRoot; } string Processed = InRef.ToString(); switch (InPathType) { case EPathType.Relative: { if (InRef.IsUnderDirectory(InRelativeRoot)) { Processed = InRef.MakeRelativeTo(InRelativeRoot).ToString(); } break; } default: { break; } } if (HostPlatform == UnrealTargetPlatform.Win64) { Processed = Processed.Replace("/", "\\"); } else { Processed = Processed.Replace('\\', '/'); } return Processed; } private string MakeQuotedPathString(FileSystemReference InRef, EPathType InPathType, DirectoryReference? InRelativeRoot = null, EQuoteType InQuoteType = EQuoteType.Double) { string Processed = CommonMakePathString(InRef, InPathType, InRelativeRoot); if (Processed.Contains(' ')) { if (HostPlatform == UnrealTargetPlatform.Win64 && InQuoteType == EQuoteType.Double) { Processed = "\"" + Processed + "\""; } else { Processed = "'" + Processed + "'"; } } return Processed; } private string MakeUnquotedPathString(FileSystemReference InRef, EPathType InPathType, DirectoryReference? InRelativeRoot = null) { return CommonMakePathString(InRef, InPathType, InRelativeRoot); } private string MakePathString(FileSystemReference InRef, bool bInAbsolute = false, bool bForceSkipQuotes = false) { if (bForceSkipQuotes) { return MakeUnquotedPathString(InRef, bInAbsolute ? EPathType.Absolute : EPathType.Relative, ProjectRoot); } else { return MakeQuotedPathString(InRef, bInAbsolute ? EPathType.Absolute : EPathType.Relative, ProjectRoot); } } public VSCodeProjectFileGenerator(FileReference? InOnlyGameProject) : base(InOnlyGameProject) { ProjectRoot = Unreal.RootDirectory; } class JsonFile { public JsonFile() { } public void BeginRootObject() { BeginObject(); } public void EndRootObject() { EndObject(); if (TabString.Length > 0) { throw new Exception("Called EndRootObject before all objects and arrays have been closed"); } } public void BeginObject(string? Name = null) { string Prefix = Name == null ? "" : Quoted(JsonWriter.EscapeString(Name)) + ": "; Lines.Add(TabString + Prefix + "{"); TabString += "\t"; } public void EndObject() { Lines[^1] = Lines[^1].TrimEnd(','); TabString = TabString.Remove(TabString.Length - 1); Lines.Add(TabString + "},"); } public void BeginArray(string? Name = null) { string Prefix = Name == null ? "" : Quoted(JsonWriter.EscapeString(Name)) + ": "; Lines.Add(TabString + Prefix + "["); TabString += "\t"; } public void EndArray() { Lines[^1] = Lines[^1].TrimEnd(','); TabString = TabString.Remove(TabString.Length - 1); Lines.Add(TabString + "],"); } public void AddField(string Name, bool Value) { Lines.Add(TabString + Quoted(JsonWriter.EscapeString(Name)) + ": " + Value.ToString().ToLower() + ","); } public void AddField(string Name, string Value) { Lines.Add(TabString + Quoted(JsonWriter.EscapeString(Name)) + ": " + Quoted(JsonWriter.EscapeString(Value)) + ","); } public void AddUnnamedField(string Value) { Lines.Add(TabString + Quoted(JsonWriter.EscapeString(Value)) + ","); } public void Write(FileReference File) { Lines[^1] = Lines[^1].TrimEnd(','); FileReference.WriteAllLines(File, Lines.ToArray()); } private string Quoted(string Value) { return "\"" + Value + "\""; } private List Lines = new List(); private string TabString = ""; } public override string ProjectFileExtension => ".vscode"; public override void CleanProjectFiles(DirectoryReference InPrimaryProjectDirectory, string InPrimaryProjectName, DirectoryReference InIntermediateProjectFilesPath, ILogger Logger) { } public override bool ShouldGenerateIntelliSenseData() { return !bNoCompileCommands && !bUseVSCodeExtension; } protected override ProjectFile AllocateProjectFile(FileReference InitFilePath, DirectoryReference BaseDir) { return new VSCodeProject(InitFilePath, BaseDir); } protected override bool WritePrimaryProjectFile(ProjectFile? UBTProject, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger) { DirectoryReference VSCodeDir = DirectoryReference.Combine(PrimaryProjectPath, ".vscode"); DirectoryReference.CreateDirectory(VSCodeDir); bForeignProject = !VSCodeDir.IsUnderDirectory(ProjectRoot); List Projects; if (bForeignProject) { Projects = new List(); foreach (ProjectFile Project in AllProjectFiles) { if (GameProjectName == Project.ProjectFilePath.GetFileNameWithoutAnyExtensions()) { Projects.Add(Project); break; } } } else { Projects = new List(AllProjectFiles); } Projects.Sort((A, B) => { return A.ProjectFilePath.GetFileName().CompareTo(B.ProjectFilePath.GetFileName()); }); ProjectData ProjectData = GatherProjectData(Projects, Logger); WriteWorkspaceIgnoreFile(Projects); WriteCppPropertiesFile(VSCodeDir, ProjectData); WriteWorkspaceFile(ProjectData, Logger); if (bForeignProject && bIncludeEngineSource) { // for installed builds we need to write the cpp properties file under the installed engine as well for intellisense to work DirectoryReference VsCodeDirectory = DirectoryReference.Combine(Unreal.RootDirectory, ".vscode"); WriteCppPropertiesFile(VsCodeDirectory, ProjectData); } return true; } private class BuildTarget { public readonly string Name; public readonly TargetType Type; public readonly UnrealTargetPlatform Platform; public readonly UnrealTargetConfiguration Configuration; public readonly CppStandardVersion CppStandard; public readonly FileReference? CompilerPath; public readonly DirectoryReference? SysRootPath; public readonly Dictionary ModuleCommandLines; public BuildTarget(string InName, TargetType InType, UnrealTargetPlatform InPlatform, UnrealTargetConfiguration InConfiguration, CppStandardVersion InCppStandard, FileReference? InCompilerPath, DirectoryReference? InSysRootPath, Dictionary InModulesCommandLines) { Name = InName; Type = InType; Platform = InPlatform; Configuration = InConfiguration; CppStandard = InCppStandard; CompilerPath = InCompilerPath; SysRootPath = InSysRootPath; ModuleCommandLines = InModulesCommandLines; } public override string ToString() { return Name.ToString() + " " + Type.ToString(); } } protected override void AddTargetForIntellisense(UEBuildTarget Target, ILogger Logger) { base.AddTargetForIntellisense(Target, Logger); bool UsingClang = true; FileReference? CompilerPath = null; DirectoryReference? SysRootPath = null; if (OperatingSystem.IsWindows()) { VCEnvironment Environment = VCEnvironment.Create(WindowsPlatform.GetDefaultCompiler(null, Target.Rules.WindowsPlatform.Architecture, Logger, true), WindowsCompiler.Default, Target.Platform, Target.Rules.WindowsPlatform.Architecture, null, null, Target.Rules.WindowsPlatform.WindowsSdkVersion, null, Target.Rules.WindowsPlatform.bUseCPPWinRT, Target.Rules.WindowsPlatform.bAllowClangLinker, Target.Rules.WindowsPlatform.bAllowRadLinker, Logger); CompilerPath = FileReference.FromString(Environment.CompilerPath.FullName); UsingClang = false; } else if (OperatingSystem.IsLinux()) { CompilerPath = FileReference.FromString(LinuxCommon.WhichClang(Logger)); string? InternalSDKPath = UEBuildPlatform.GetSDK(UnrealTargetPlatform.Linux)?.GetInternalSDKPath(); if (!String.IsNullOrEmpty(InternalSDKPath)) { SysRootPath = DirectoryReference.FromString(InternalSDKPath); } } else if (OperatingSystem.IsMacOS()) { MacToolChainSettings Settings = new MacToolChainSettings(false, Logger); CompilerPath = FileReference.Combine(Settings.ToolchainDir, "clang++"); SysRootPath = Settings.GetSDKPath(UnrealArch.Host.Value); } else { throw new Exception("Unknown platform " + HostPlatform.ToString()); } // we do not need to keep track of which binary the invocation belongs to, only which target, as such we join all binaries into a single set Dictionary ModuleDirectoryToCompileCommand = new Dictionary(); // Generate a compile environment for each module in the binary CppCompileEnvironment GlobalCompileEnvironment = Target.CreateCompileEnvironmentForProjectFiles(Logger); foreach (UEBuildBinary Binary in Target.Binaries) { CppCompileEnvironment BinaryCompileEnvironment = Binary.CreateBinaryCompileEnvironment(GlobalCompileEnvironment); foreach (UEBuildModuleCPP Module in Binary.Modules.OfType()) { CppCompileEnvironment ModuleCompileEnvironment = Module.CreateCompileEnvironmentForIntellisense(Target.Rules, BinaryCompileEnvironment, Logger); List ForceIncludePaths = new List(ModuleCompileEnvironment.ForceIncludeFiles.Select(x => x.Location)); if (ModuleCompileEnvironment.PrecompiledHeaderIncludeFilename != null) { ForceIncludePaths.Add(ModuleCompileEnvironment.PrecompiledHeaderIncludeFilename); } StringBuilder CommandBuilder = new StringBuilder(); foreach (FileReference ForceIncludeFile in ForceIncludePaths) { CommandBuilder.AppendFormat("{0} \"{1}\" {2}", UsingClang ? "-include" : "/FI", ForceIncludeFile.FullName, Environment.NewLine); } foreach (string Definition in ModuleCompileEnvironment.Definitions) { CommandBuilder.AppendFormat("{0} \"{1}\" {2}", UsingClang ? "-D" : "/D", Definition, Environment.NewLine); } foreach (DirectoryReference IncludePath in ModuleCompileEnvironment.UserIncludePaths) { CommandBuilder.AppendFormat("{0} \"{1}\" {2}", UsingClang ? "-I" : "/I", IncludePath, Environment.NewLine); } foreach (DirectoryReference IncludePath in ModuleCompileEnvironment.SystemIncludePaths) { CommandBuilder.AppendFormat("{0} \"{1}\" {2}", UsingClang ? "-I" : "/I", IncludePath, Environment.NewLine); } ModuleDirectoryToCompileCommand.TryAdd(Module.ModuleDirectory, CommandBuilder.ToString()); } } BuildTargets.Add(new BuildTarget(Target.TargetName, Target.TargetType, Target.Platform, Target.Configuration, GlobalCompileEnvironment.CppStandard, CompilerPath, SysRootPath, ModuleDirectoryToCompileCommand)); } private class ProjectData { public enum EOutputType { Library, Exe, WinExe, // some projects have this so we need to read it, but it will be converted across to Exe so no code should handle it! } public class BuildProduct { public FileReference OutputFile { get; set; } public FileReference? UProjectFile { get; set; } public UnrealTargetConfiguration Config { get; set; } public UnrealTargetPlatform Platform { get; set; } public EOutputType OutputType { get; set; } public CsProjectInfo? CSharpInfo { get; set; } public override string ToString() { return Platform.ToString() + " " + Config.ToString(); } public BuildProduct(FileReference OutputFile) { this.OutputFile = OutputFile; } } public class Target { public string Name; public TargetType Type; public List BuildProducts = new List(); public Target(Project InParentProject, string InName, TargetType InType) { Name = InName; Type = InType; InParentProject.Targets.Add(this); } public override string ToString() { return Name.ToString() + " " + Type.ToString(); } } public class Project { public string Name; public ProjectFile SourceProject; public List Targets = new List(); public override string ToString() { return Name; } public Project(string Name, ProjectFile SourceProject) { this.Name = Name; this.SourceProject = SourceProject; } } public List NativeProjects = new List(); public List CSharpProjects = new List(); public List AllProjects = new List(); } private ProjectData GatherProjectData(List InProjects, ILogger Logger) { ProjectData ProjectData = new ProjectData(); foreach (ProjectFile Project in InProjects) { // Create new project record ProjectData.Project NewProject = new ProjectData.Project(Project.ProjectFilePath.GetFileNameWithoutExtension(), Project); ProjectData.AllProjects.Add(NewProject); // Add into the correct easy-access list if (Project is VSCodeProject) { foreach (ProjectTarget Target in Project.ProjectTargets.OfType()) { UnrealTargetConfiguration[] Configs = (UnrealTargetConfiguration[])Enum.GetValues(typeof(UnrealTargetConfiguration)); List Platforms = new List(Target.TargetRules!.GetSupportedPlatforms()); ProjectData.Target NewTarget = new ProjectData.Target(NewProject, Target.TargetRules.Name, Target.TargetRules.Type); if (HostPlatform != UnrealTargetPlatform.Win64) { Platforms.Remove(UnrealTargetPlatform.Win64); } foreach (UnrealTargetPlatform Platform in Platforms) { UEBuildPlatform.TryGetBuildPlatform(Platform, out UEBuildPlatform? BuildPlatform); if (SupportedPlatforms.Contains(Platform) && (BuildPlatform != null) && (BuildPlatform.HasRequiredSDKsInstalled() == SDKStatus.Valid)) { foreach (UnrealTargetConfiguration Config in Configs) { if (MSBuildProjectFile.IsValidProjectPlatformAndConfiguration(Target, Platform, Config, Logger)) { NewTarget.BuildProducts.Add(new ProjectData.BuildProduct(GetExecutableFilename(Project, Target, Platform, Config)) { Platform = Platform, Config = Config, UProjectFile = Target.UnrealProjectFilePath, OutputType = ProjectData.EOutputType.Exe, CSharpInfo = null }); } } } } } ProjectData.NativeProjects.Add(NewProject); } else { VCSharpProjectFile VCSharpProject = (VCSharpProjectFile)Project; string ProjectName = Project.ProjectFilePath.GetFileNameWithoutExtension(); ProjectData.Target Target = new ProjectData.Target(NewProject, ProjectName, TargetType.Program); UnrealTargetConfiguration[] Configs = { UnrealTargetConfiguration.Debug, UnrealTargetConfiguration.Development }; foreach (UnrealTargetConfiguration Config in Configs) { CsProjectInfo? Info = VCSharpProject.GetProjectInfo(Config)!; { ProjectData.EOutputType OutputType; string? OutputTypeName; if (Info.Properties.TryGetValue("OutputType", out OutputTypeName)) { OutputType = (ProjectData.EOutputType)Enum.Parse(typeof(ProjectData.EOutputType), OutputTypeName); } else { OutputType = ProjectData.EOutputType.Library; } if (OutputType == ProjectData.EOutputType.WinExe) { OutputType = ProjectData.EOutputType.Exe; } DirectoryReference OutputPath = Info.GetOutputDir(VCSharpProject.ProjectFilePath.Directory); FileReference? OutputFile = null; HashSet ProjectBuildProducts = new HashSet(); Info.FindCompiledBuildProducts(OutputPath, ProjectBuildProducts); foreach (FileReference ProjectBuildProduct in ProjectBuildProducts) { if ((OutputType == ProjectData.EOutputType.Exe && ProjectBuildProduct.GetExtension() == FrameworkExecutableExtension) || (OutputType == ProjectData.EOutputType.Library && ProjectBuildProduct.GetExtension() == FrameworkLibraryExtension)) { OutputFile = ProjectBuildProduct; break; } } if (OutputFile != null) { Target.BuildProducts.Add(new ProjectData.BuildProduct(OutputFile) { Platform = HostPlatform, Config = Config, OutputType = OutputType, CSharpInfo = Info }); } } } ProjectData.CSharpProjects.Add(NewProject); } } return ProjectData; } private void WriteCppPropertiesFile(DirectoryReference OutputDirectory, ProjectData Projects) { DirectoryReference.CreateDirectory(OutputDirectory); JsonFile OutFile = new JsonFile(); OutFile.BeginRootObject(); { OutFile.BeginArray("configurations"); { HashSet AllSourceFiles = new HashSet(); Dictionary AllModuleCommandLines = new Dictionary(); FileReference? CompilerPath = null; DirectoryReference? SysRootPath = null; CppStandardVersion CppStandard = CppStandardVersion.Default; foreach (ProjectData.Project Project in Projects.AllProjects) { AllSourceFiles.UnionWith(Project.SourceProject.SourceFiles.Select(x => x.Reference)); foreach (ProjectData.Target ProjectTarget in Project.Targets) { BuildTarget? BuildTarget = BuildTargets.FirstOrDefault(Target => Target.Name == ProjectTarget.Name); // we do not generate intellisense for every target, as that just causes a lot of redundancy, as such we will not find a mapping for a lot of the targets if (BuildTarget == null) { continue; } string Name = String.Format("{0} {1} {2} {3} ({4})", ProjectTarget.Name, ProjectTarget.Type, BuildTarget.Platform, BuildTarget.Configuration, Project.Name); WriteConfiguration(Name, Project.Name, Project.SourceProject.SourceFiles.Select(x => x.Reference), BuildTarget.CppStandard, BuildTarget.CompilerPath!, BuildTarget.SysRootPath, BuildTarget.ModuleCommandLines, OutFile, OutputDirectory); CompilerPath = BuildTarget.CompilerPath; foreach (KeyValuePair Pair in BuildTarget.ModuleCommandLines) { if (!AllModuleCommandLines.ContainsKey(Pair.Key)) { AllModuleCommandLines[Pair.Key] = Pair.Value; } } if (BuildTarget.CppStandard > CppStandard) { CppStandard = BuildTarget.CppStandard; } if (BuildTarget.SysRootPath != null) { SysRootPath = BuildTarget.SysRootPath; } } } string DefaultConfigName; if (HostPlatform == UnrealTargetPlatform.Linux) { DefaultConfigName = "Linux"; } else if (HostPlatform == UnrealTargetPlatform.Mac) { DefaultConfigName = "Mac"; } else { DefaultConfigName = "Win32"; } WriteConfiguration(DefaultConfigName, "Default", AllSourceFiles, CppStandard, CompilerPath!, SysRootPath, AllModuleCommandLines, OutFile, OutputDirectory); } OutFile.EndArray(); } OutFile.EndRootObject(); OutFile.Write(FileReference.Combine(OutputDirectory, "c_cpp_properties.json")); } private void WriteConfiguration(string Name, string ProjectName, IEnumerable SourceFiles, CppStandardVersion CppStandard, FileReference CompilerPath, DirectoryReference? SysRootPath, Dictionary ModuleCommandLines, JsonFile OutFile, DirectoryReference OutputDirectory) { OutFile.BeginObject(); OutFile.AddField("name", Name); if (CompilerPath != null) { OutFile.AddField("compilerPath", CompilerPath.FullName); } if (HostPlatform == UnrealTargetPlatform.Mac) { string SysRoot = SysRootPath != null ? SysRootPath.FullName : "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"; OutFile.BeginArray("compilerArgs"); { OutFile.AddUnnamedField("-isysroot"); OutFile.AddUnnamedField(SysRoot); } OutFile.EndArray(); OutFile.BeginArray("macFrameworkPath"); { OutFile.AddUnnamedField(SysRoot + "/System/Library/Frameworks"); } OutFile.EndArray(); } else if (SysRootPath != null) { OutFile.BeginArray("compilerArgs"); { OutFile.AddUnnamedField("-isysroot"); OutFile.AddUnnamedField(SysRootPath.FullName); } OutFile.EndArray(); } OutFile.AddField("cStandard", "c17"); switch (CppStandard) { case CppStandardVersion.Cpp20: OutFile.AddField("cppStandard", "c++20"); break; case CppStandardVersion.Cpp23: OutFile.AddField("cppStandard", "c++23"); break; case CppStandardVersion.Latest: default: throw new BuildException($"Unsupported C++ standard type set: {CppStandard}"); } string CompilerPrefix = HostPlatform == UnrealTargetPlatform.Win64 ? "msvc" : "clang"; string ArchSuffix = UnrealArch.Host.Value.bIsX64 ? "x64" : "arm64"; // make msvc-x64, clang-arm64, etc OutFile.AddField("intelliSenseMode", $"{CompilerPrefix}-{ArchSuffix}"); if (bUseVSCodeExtension) { OutFile.AddField("configurationProvider", "epic.ue"); } if (ShouldGenerateIntelliSenseData()) { FileReference CompileCommands = FileReference.Combine(OutputDirectory, String.Format("compileCommands_{0}.json", ProjectName)); WriteCompileCommands(CompileCommands, SourceFiles, CompilerPath!, ModuleCommandLines); OutFile.AddField("compileCommands", MakePathString(CompileCommands, bInAbsolute: true, bForceSkipQuotes: true)); } OutFile.EndObject(); } private void WriteNativeTaskDeployAndroid(ProjectData.Project InProject, JsonFile OutFile, ProjectData.Target Target, ProjectData.BuildProduct BuildProduct) { if (BuildProduct.UProjectFile == null) { return; } string[] ConfigTypes = new string[] { "Cook+Deploy", "Cook", "Deploy" }; foreach (string ConfigType in ConfigTypes) { OutFile.BeginObject(); { string TaskName = String.Format("{0} {1} {2} {3}", Target.Name, BuildProduct.Platform.ToString(), BuildProduct.Config, ConfigType); OutFile.AddField("label", TaskName); OutFile.AddField("group", "build"); if (HostPlatform == UnrealTargetPlatform.Win64) { OutFile.AddField("command", MakePathString(FileReference.Combine(ProjectRoot, "Engine", "Build", "BatchFiles", "RunUAT.bat"))); } else { OutFile.AddField("command", MakePathString(FileReference.Combine(ProjectRoot, "Engine", "Build", "BatchFiles", "RunUAT.sh"))); } OutFile.BeginArray("args"); { OutFile.AddUnnamedField("BuildCookRun"); OutFile.AddUnnamedField("-ScriptsForProject=" + BuildProduct.UProjectFile.ToNormalizedPath()); OutFile.AddUnnamedField("-Project=" + BuildProduct.UProjectFile.ToNormalizedPath()); OutFile.AddUnnamedField("-noP4"); OutFile.AddUnnamedField(String.Format("-ClientConfig={0}", BuildProduct.Config.ToString())); OutFile.AddUnnamedField(String.Format("-ServerConfig={0}", BuildProduct.Config.ToString())); OutFile.AddUnnamedField("-NoCompileEditor"); OutFile.AddUnnamedField("-utf8output"); OutFile.AddUnnamedField(String.Format("-Platform={0}", BuildProduct.Platform.ToString())); OutFile.AddUnnamedField(String.Format("-TargetPlatform={0}", BuildProduct.Platform.ToString())); OutFile.AddUnnamedField("-ini:Game:[/Script/UnrealEd.ProjectPackagingSettings]:BlueprintNativizationMethod=Disabled"); OutFile.AddUnnamedField("-Compressed"); OutFile.AddUnnamedField("-IterativeCooking"); OutFile.AddUnnamedField("-IterativeDeploy"); switch (ConfigType) { case "Cook+Deploy": { OutFile.AddUnnamedField("-Cook"); OutFile.AddUnnamedField("-Stage"); OutFile.AddUnnamedField("-Deploy"); break; } case "Cook": { OutFile.AddUnnamedField("-Cook"); break; } case "Deploy": { OutFile.AddUnnamedField("-DeploySoToDevice"); OutFile.AddUnnamedField("-SkipCook"); OutFile.AddUnnamedField("-Stage"); OutFile.AddUnnamedField("-Deploy"); break; } } } OutFile.EndArray(); OutFile.BeginArray("dependsOn"); { switch (ConfigType) { case "Cook+Deploy": case "Cook": { OutFile.AddUnnamedField(String.Format("{0}Editor {1} Development Build", Target.Name, HostPlatform.ToString())); OutFile.AddUnnamedField(String.Format("{0} {1} {2} Build", Target.Name, BuildProduct.Platform.ToString(), BuildProduct.Config)); break; } default: { OutFile.AddUnnamedField(String.Format("{0} {1} {2} Build", Target.Name, BuildProduct.Platform.ToString(), BuildProduct.Config)); break; } } } OutFile.EndArray(); OutFile.AddField("type", "shell"); OutFile.BeginObject("options"); { OutFile.AddField("cwd", MakeUnquotedPathString(ProjectRoot, EPathType.Absolute)); } OutFile.EndObject(); } OutFile.EndObject(); } } private void WriteCompileCommands(FileReference CompileCommandsFile, IEnumerable SourceFiles, FileReference CompilerPath, Dictionary ModuleCommandLines) { // this creates a compileCommands.json // see VsCode Docs - https://code.visualstudio.com/docs/cpp/c-cpp-properties-schema-reference (compileCommands attribute) // and the clang format description https://clang.llvm.org/docs/JSONCompilationDatabase.html using (JsonWriter Writer = new JsonWriter(CompileCommandsFile)) { Writer.WriteArrayStart(); DirectoryReference ResponseFileDir = DirectoryReference.Combine(CompileCommandsFile.Directory, CompileCommandsFile.GetFileNameWithoutExtension()); DirectoryReference.CreateDirectory(ResponseFileDir); Dictionary DirectoryToResponseFile = new Dictionary(); foreach (KeyValuePair Pair in ModuleCommandLines) { FileReference ResponseFile = FileReference.Combine(ResponseFileDir, String.Format("{0}.{1}.rsp", Pair.Key.GetDirectoryName(), DirectoryToResponseFile.Count)); FileReference.WriteAllText(ResponseFile, Pair.Value); DirectoryToResponseFile.Add(Pair.Key, ResponseFile); } foreach (FileReference File in SourceFiles.OrderBy(x => x.FullName)) { DirectoryReference Directory = File.Directory; FileReference? ResponseFile = null; if (!DirectoryToResponseFile.TryGetValue(Directory, out ResponseFile)) { for (DirectoryReference? ParentDir = Directory; ParentDir != null && ParentDir != Unreal.RootDirectory; ParentDir = ParentDir.ParentDirectory) { if (DirectoryToResponseFile.TryGetValue(ParentDir, out ResponseFile)) { break; } } DirectoryToResponseFile[Directory] = ResponseFile; } if (ResponseFile == null) { // no compiler command associated with the file, will happen for any file that is not a C++ file and is not an error continue; } Writer.WriteObjectStart(); Writer.WriteValue("file", MakePathString(File, bInAbsolute: true, bForceSkipQuotes: true)); Writer.WriteArrayStart("arguments"); Writer.WriteValue(MakePathString(CompilerPath, bInAbsolute: true, bForceSkipQuotes: true)); Writer.WriteValue($"@{MakePathString(ResponseFile, bInAbsolute: true, bForceSkipQuotes: true)}"); Writer.WriteArrayEnd(); Writer.WriteValue("directory", Unreal.EngineSourceDirectory.ToString()); Writer.WriteObjectEnd(); } Writer.WriteArrayEnd(); } } private void WriteNativeTask(ProjectData.Project InProject, JsonFile OutFile) { string[] Commands = { "Build", "Rebuild", "Clean" }; foreach (ProjectData.Target Target in InProject.Targets) { foreach (ProjectData.BuildProduct BuildProduct in Target.BuildProducts) { foreach (string BaseCommand in Commands) { string Command = BaseCommand == "Rebuild" ? "Build" : BaseCommand; string TaskName = String.Format("{0} {1} {2} {3}", Target.Name, BuildProduct.Platform.ToString(), BuildProduct.Config, BaseCommand); string CleanTaskName = String.Format("{0} {1} {2} {3}", Target.Name, BuildProduct.Platform.ToString(), BuildProduct.Config, "Clean"); OutFile.BeginObject(); { OutFile.AddField("label", TaskName); OutFile.AddField("group", "build"); string? CleanParam = Command == "Clean" ? "-clean" : null; if (HostPlatform == UnrealTargetPlatform.Win64) { OutFile.AddField("command", MakePathString(FileReference.Combine(ProjectRoot, "Engine", "Build", "BatchFiles", Command + ".bat"))); CleanParam = null; } else { OutFile.AddField("command", MakePathString(FileReference.Combine(ProjectRoot, "Engine", "Build", "BatchFiles", HostPlatform.ToString(), "Build.sh"))); if (Command == "Clean") { CleanParam = "-clean"; } } OutFile.BeginArray("args"); { OutFile.AddUnnamedField(Target.Name); OutFile.AddUnnamedField(BuildProduct.Platform.ToString()); OutFile.AddUnnamedField(BuildProduct.Config.ToString()); if (bForeignProject) { OutFile.AddUnnamedField(MakeUnquotedPathString(BuildProduct.UProjectFile!, EPathType.Relative, null)); } OutFile.AddUnnamedField("-waitmutex"); if (!String.IsNullOrEmpty(CleanParam)) { OutFile.AddUnnamedField(CleanParam); } } OutFile.EndArray(); OutFile.AddField("problemMatcher", "$msCompile"); if (!bForeignProject || BaseCommand == "Rebuild") { OutFile.BeginArray("dependsOn"); { if (!bForeignProject) { if (Command == "Build" && Target.Type == TargetType.Editor) { OutFile.AddUnnamedField("ShaderCompileWorker " + HostPlatform.ToString() + " Development Build"); } else { OutFile.AddUnnamedField("UnrealBuildTool " + HostPlatform.ToString() + " Development Build"); } } if (BaseCommand == "Rebuild") { OutFile.AddUnnamedField(CleanTaskName); } } OutFile.EndArray(); } OutFile.AddField("type", "shell"); OutFile.BeginObject("options"); { OutFile.AddField("cwd", MakeUnquotedPathString(ProjectRoot, EPathType.Absolute)); } OutFile.EndObject(); } OutFile.EndObject(); if (BuildProduct.Platform == UnrealTargetPlatform.Android && BaseCommand.Equals("Build")) { WriteNativeTaskDeployAndroid(InProject, OutFile, Target, BuildProduct); } } } } } private void WriteCSharpTask(ProjectData.Project InProject, JsonFile OutFile) { string[] Commands = { "Build", "Clean" }; foreach (ProjectData.Target Target in InProject.Targets) { foreach (ProjectData.BuildProduct BuildProduct in Target.BuildProducts) { foreach (string Command in Commands) { string TaskName = String.Format("{0} {1} {2} {3}", Target.Name, BuildProduct.Platform, BuildProduct.Config, Command); OutFile.BeginObject(); { OutFile.AddField("label", TaskName); OutFile.AddField("group", "build"); OutFile.AddField("command", MakePathString(Unreal.DotnetPath)); OutFile.BeginArray("args"); { OutFile.AddUnnamedField(Command.ToLower()); OutFile.AddUnnamedField("--configuration"); OutFile.AddUnnamedField(BuildProduct.Config.ToString()); OutFile.AddUnnamedField(MakeUnquotedPathString(BuildProduct.CSharpInfo!.ProjectPath, EPathType.Absolute)); } OutFile.EndArray(); OutFile.AddField("problemMatcher", "$msCompile"); OutFile.AddField("type", "shell"); OutFile.BeginObject("options"); { OutFile.AddField("cwd", MakeUnquotedPathString(ProjectRoot, EPathType.Absolute)); OutFile.BeginObject("env"); { OutFile.AddField("PATH", $"{Unreal.DotnetDirectory}{Path.PathSeparator}${{env:PATH}}");; OutFile.AddField("DOTNET_MULTILEVEL_LOOKUP", "0"); OutFile.AddField("DOTNET_ROLL_FORWARD", "LatestMajor"); } OutFile.EndObject(); } OutFile.EndObject(); } OutFile.EndObject(); } } } if (InProject.Name.Equals("AutomationTool", StringComparison.OrdinalIgnoreCase)) { WriteAutomationScriptTask(InProject, OutFile); } } private void WriteAutomationScriptTask(ProjectData.Project InProject, JsonFile OutFile) { string[] Commands = { "Build" }; foreach (ProjectData.Target Target in InProject.Targets) { foreach (ProjectData.BuildProduct BuildProduct in Target.BuildProducts.Where(x => x.OutputType == ProjectData.EOutputType.Exe)) { foreach (string Command in Commands) { string TaskName = String.Format("{0}+Scripts {1} {2} {3}", Target.Name, BuildProduct.Platform, BuildProduct.Config, Command); string DependsOnTaskName = String.Format("{0} {1} {2} {3}", Target.Name, BuildProduct.Platform, BuildProduct.Config, Command); OutFile.BeginObject(); { OutFile.AddField("label", TaskName); OutFile.AddField("group", "build"); OutFile.AddField("command", MakePathString(BuildProduct.OutputFile)); OutFile.BeginArray("args"); { OutFile.AddUnnamedField("-list"); } OutFile.EndArray(); OutFile.BeginArray("dependsOn"); { OutFile.AddUnnamedField(DependsOnTaskName); } OutFile.EndArray(); OutFile.AddField("problemMatcher", "$msCompile"); OutFile.AddField("type", "shell"); OutFile.BeginObject("options"); { OutFile.AddField("cwd", MakeUnquotedPathString(ProjectRoot, EPathType.Absolute)); OutFile.BeginObject("env"); { OutFile.AddField("PATH", $"{Unreal.DotnetDirectory}{Path.PathSeparator}${{env:PATH}}"); OutFile.AddField("DOTNET_MULTILEVEL_LOOKUP", "0"); OutFile.AddField("DOTNET_ROLL_FORWARD", "LatestMajor"); } OutFile.EndObject(); } OutFile.EndObject(); } OutFile.EndObject(); } } } } private void WriteTasks(JsonFile OutFile, ProjectData ProjectData) { OutFile.AddField("version", "2.0.0"); OutFile.BeginArray("tasks"); { if (!bUseVSCodeExtension) { foreach (ProjectData.Project NativeProject in ProjectData.NativeProjects) { WriteNativeTask(NativeProject, OutFile); } } foreach (ProjectData.Project CSharpProject in ProjectData.CSharpProjects) { WriteCSharpTask(CSharpProject, OutFile); } OutFile.EndArray(); } } private FileReference GetExecutableFilename(ProjectFile Project, ProjectTarget Target, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration) { TargetRules? TargetRulesObject = Target.TargetRules; FileReference TargetFilePath = Target.TargetFilePath; string TargetName = TargetFilePath == null ? Project.ProjectFilePath.GetFileNameWithoutExtension() : TargetFilePath.GetFileNameWithoutAnyExtensions(); string UBTPlatformName = Platform.ToString(); // Setup output path UEBuildPlatform BuildPlatform = UEBuildPlatform.GetBuildPlatform(Platform); // Figure out if this is a monolithic build bool bShouldCompileMonolithic = BuildPlatform.ShouldCompileMonolithicBinary(Platform); if (TargetRulesObject != null) { try { bShouldCompileMonolithic |= (Target.CreateRulesDelegate(Platform, Configuration).LinkType == TargetLinkType.Monolithic); } catch (BuildException) { } } TargetType TargetRulesType = Target.TargetRules == null ? TargetType.Program : Target.TargetRules.Type; // Get the output directory DirectoryReference RootDirectory = Unreal.EngineDirectory; if (TargetRulesType != TargetType.Program && (bShouldCompileMonolithic || TargetRulesObject!.BuildEnvironment == TargetBuildEnvironment.Unique)) { if (Target.UnrealProjectFilePath != null) { RootDirectory = Target.UnrealProjectFilePath.Directory; } } if (TargetRulesType == TargetType.Program) { if (Target.UnrealProjectFilePath != null) { RootDirectory = Target.UnrealProjectFilePath.Directory; } } // Get the output directory DirectoryReference OutputDirectory = DirectoryReference.Combine(RootDirectory, "Binaries", UBTPlatformName); // Get the executable name (minus any platform or config suffixes) string BinaryName; if (Target.TargetRules!.BuildEnvironment == TargetBuildEnvironment.Shared && TargetRulesType != TargetType.Program) { BinaryName = UEBuildTarget.GetAppNameForTargetType(TargetRulesType); } else { BinaryName = TargetName; } // Make the output file path string BinaryFileName = UEBuildTarget.MakeBinaryFileName(BinaryName, TargetRulesObject!.DecoratedSeparator, Platform, Configuration, TargetRulesObject!.Architectures, TargetRulesObject.UndecoratedConfiguration, UEBuildBinaryType.Executable); string ExecutableFilename = FileReference.Combine(OutputDirectory, BinaryFileName).FullName; // Include the path to the actual executable for a Mac app bundle if (Platform == UnrealTargetPlatform.Mac && !Target.TargetRules.bIsBuildingConsoleApplication) { ExecutableFilename += ".app/Contents/MacOS/" + Path.GetFileName(ExecutableFilename); } return new FileReference(ExecutableFilename); } private void WriteNativeLaunchConfigAndroidOculus(ProjectData.Project InProject, JsonFile OutFile, ProjectData.Target Target, ProjectData.BuildProduct BuildProduct, ILogger Logger) { ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(BuildProduct.UProjectFile), BuildProduct.Platform); bool ArrayResult = Ini.GetArray("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "PackageForOculusMobile", out List? OculusMobileDevices); // Backcompat for deprecated oculus device target setting bool BoolResult = Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bPackageForMetaQuest", out bool bPackageForMetaQuest); // Check if packaging for Meta Quest if ((!ArrayResult || OculusMobileDevices == null || OculusMobileDevices.Count == 0) && (!BoolResult || !bPackageForMetaQuest)) { return; } // Get package name string PackageName; Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "PackageName", out PackageName); if (PackageName.Contains("[PROJECT]")) { // project name must start with a letter if (!Char.IsLetter(Target.Name[0])) { Trace.TraceWarning("Package name segments must all start with a letter. Please replace [PROJECT] with a valid name"); } string ProjectName = Target.Name; // hyphens not allowed so change them to underscores in project name if (ProjectName.Contains('-')) { Trace.TraceWarning("Project name contained hyphens, converted to underscore"); ProjectName = ProjectName.Replace("-", "_"); } // check for special characters for (int Index = 0; Index < ProjectName.Length; Index++) { char c = ProjectName[Index]; if (c != '.' && c != '_' && !Char.IsLetterOrDigit(c)) { Trace.TraceWarning("Project name contains illegal characters (only letters, numbers, and underscore allowed); please replace [PROJECT] with a valid name"); ProjectName.Replace(c, '_'); } } PackageName = PackageName.Replace("[PROJECT]", ProjectName); } // Get store version int StoreVersion = 1; int StoreVersionArm64 = 1; int StoreVersionOffsetArm64 = 0; Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "StoreVersion", out StoreVersion); Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "StoreVersionOffsetArm64", out StoreVersionOffsetArm64); StoreVersionArm64 = StoreVersion + StoreVersionOffsetArm64; DirectoryReference SymbolPathArm64 = DirectoryReference.Combine( BuildProduct.OutputFile.Directory, Target.Name + "_Symbols_v" + StoreVersionArm64.ToString(), Target.Name + "-arm64"); string LaunchTaskName = String.Format("{0} {1} {2} Deploy", Target.Name, BuildProduct.Platform, BuildProduct.Config); List ConfigTypes = new List(); ConfigTypes.Add("Launch"); if (BuildProduct.Config == UnrealTargetConfiguration.Development) { ConfigTypes.Add("Attach"); } foreach (string ConfigType in ConfigTypes) { OutFile.BeginObject(); { OutFile.AddField("name", Target.Name + " Oculus (" + BuildProduct.Config.ToString() + ") " + ConfigType); OutFile.AddField("request", ConfigType.ToLowerInvariant()); if (ConfigType == "Launch") { OutFile.AddField("preLaunchTask", LaunchTaskName); } OutFile.AddField("type", "fb-lldb"); OutFile.BeginObject("android"); { OutFile.BeginObject("application"); { OutFile.AddField("package", PackageName); OutFile.AddField("activity", "com.epicgames.unreal.GameActivity"); } OutFile.EndObject(); OutFile.BeginObject("lldbConfig"); { OutFile.BeginArray("librarySearchPaths"); OutFile.AddUnnamedField("\\\"" + SymbolPathArm64.ToNormalizedPath() + "\\\""); OutFile.EndArray(); OutFile.BeginArray("lldbPreTargetCreateCommands"); FileReference DataFormatters = FileReference.Combine(ProjectRoot, "Engine", "Extras", "LLDBDataFormatters", "UEDataFormatters_2ByteChars.py"); OutFile.AddUnnamedField("command script import \\\"" + DataFormatters.FullName.Replace("\\", "/") + "\\\""); OutFile.EndArray(); OutFile.BeginArray("lldbPostTargetCreateCommands"); //on Oculus devices, we use SIGILL for input redirection, so the debugger shouldn't catch it. OutFile.AddUnnamedField("process handle --pass true --stop false --notify true SIGILL"); OutFile.EndArray(); } OutFile.EndObject(); } OutFile.EndObject(); } OutFile.EndObject(); } } private void WriteNativeLaunchConfig(ProjectData.Project InProject, JsonFile OutFile, ProjectData.Target Target, ProjectData.BuildProduct BuildProduct) { bool bIsLinux = BuildProduct.Platform == UnrealTargetPlatform.Linux; List Types = new List(); Types.Add("Launch"); if (bAddDebugAttachConfig && bIsLinux) { Types.Add("Attach"); } if (bAddDebugCoreConfig && bIsLinux) { Types.Add("Debug Core"); } string LaunchTaskName = String.Format("{0} {1} {2} Build", Target.Name, BuildProduct.Platform, BuildProduct.Config); foreach (string Type in Types) { OutFile.BeginObject(); { OutFile.AddField("name", Type + " " + Target.Name + " (" + BuildProduct.Config.ToString() + ")"); OutFile.AddField("request", (Type == "Attach") ? "attach" : "launch"); OutFile.AddField("program", MakeUnquotedPathString(BuildProduct.OutputFile, EPathType.Absolute)); switch (Type) { case "Launch": OutFile.AddField("preLaunchTask", LaunchTaskName); break; case "Debug Core": OutFile.AddField("coreDumpPath", "${input:coreFileName}"); break; case "Attach": OutFile.AddField("processId", "${command:pickProcess}"); break; } if (Type != "Attach") { OutFile.BeginArray("args"); { if (Target.Type == TargetRules.TargetType.Editor) { if (InProject.Name != "UE5") { if (bForeignProject) { OutFile.AddUnnamedField(MakePathString(BuildProduct.UProjectFile!, false, true)); } else { OutFile.AddUnnamedField(InProject.Name); } } } } OutFile.EndArray(); } /* DirectoryReference CWD = BuildProduct.OutputFile.Directory; while (HostPlatform == UnrealTargetPlatform.Mac && CWD != null && CWD.ToString().Contains(".app")) { CWD = CWD.ParentDirectory; } if (CWD != null) { OutFile.AddField("cwd", MakePathString(CWD, true, true)); } */ OutFile.AddField("cwd", MakeUnquotedPathString(ProjectRoot, EPathType.Absolute)); if (HostPlatform == UnrealTargetPlatform.Win64) { OutFile.AddField("stopAtEntry", false); OutFile.AddField("console", "integratedTerminal"); OutFile.AddField("type", "cppvsdbg"); OutFile.AddField("visualizerFile", MakeUnquotedPathString(FileReference.Combine(ProjectRoot, "Engine", "Extras", "VisualStudioDebugging", "Unreal.natvis"), EPathType.Absolute)); } else if (HostPlatform == UnrealTargetPlatform.Linux) { OutFile.AddField("type", "cppdbg"); OutFile.AddField("visualizerFile", MakeUnquotedPathString(FileReference.Combine(ProjectRoot, "Engine", "Extras", "VisualStudioDebugging", "Unreal.natvis"), EPathType.Absolute)); OutFile.AddField("showDisplayString", true); } else { OutFile.AddField("type", "lldb"); } Dictionary SourceFileMap = new(); if (Unreal.OriginalCompilationRootDirectory != ProjectRoot) { SourceFileMap.Add(Unreal.OriginalCompilationRootDirectory, ProjectRoot); } CppRootPaths RootPaths = new(); RootPaths[CppRootPathFolder.Root] = Unreal.RootDirectory; if (BuildProduct.UProjectFile != null) { RootPaths[CppRootPathFolder.Project] = BuildProduct.UProjectFile.Directory; } foreach ((uint id, DirectoryReference vfs, DirectoryReference local) entry in RootPaths) { SourceFileMap.Add(entry.vfs, entry.local); } if (SourceFileMap.Any()) { OutFile.BeginObject("sourceFileMap"); { foreach (KeyValuePair entry in SourceFileMap) { OutFile.AddField( MakeUnquotedPathString(entry.Key, EPathType.Absolute), MakeUnquotedPathString(entry.Value, EPathType.Absolute)); } } OutFile.EndObject(); } } OutFile.EndObject(); } } private void WriteNativeLaunchConfig(ProjectData.Project InProject, JsonFile OutFile, ILogger Logger) { foreach (ProjectData.Target Target in InProject.Targets) { foreach (ProjectData.BuildProduct BuildProduct in Target.BuildProducts) { if (BuildProduct.Platform == HostPlatform) { WriteNativeLaunchConfig(InProject, OutFile, Target, BuildProduct); } else if (BuildProduct.Platform == UnrealTargetPlatform.Android) { WriteNativeLaunchConfigAndroidOculus(InProject, OutFile, Target, BuildProduct, Logger); } } } } private void WriteSingleCSharpLaunchConfig(JsonFile OutFile, string InTaskName, string InBuildTaskName, FileReference InExecutable, string[]? InArgs) { OutFile.BeginObject(); { OutFile.AddField("name", InTaskName); OutFile.AddField("type", "coreclr"); OutFile.AddField("request", "launch"); if (!String.IsNullOrEmpty(InBuildTaskName)) { OutFile.AddField("preLaunchTask", InBuildTaskName); } DirectoryReference CWD = ProjectRoot; OutFile.AddField("program", MakeUnquotedPathString(InExecutable, EPathType.Absolute)); OutFile.BeginArray("args"); { if (InArgs != null) { foreach (string Arg in InArgs) { OutFile.AddUnnamedField(Arg); } } } OutFile.EndArray(); OutFile.BeginObject("env"); { OutFile.AddField("PATH", $"{Unreal.DotnetDirectory}{Path.PathSeparator}${{env:PATH}}"); OutFile.AddField("DOTNET_ROOT", Unreal.DotnetDirectory.FullName); OutFile.AddField("DOTNET_HOST_PATH", Unreal.DotnetPath.FullName); OutFile.AddField("DOTNET_MULTILEVEL_LOOKUP", "0"); OutFile.AddField("DOTNET_ROLL_FORWARD", "LatestMajor"); } OutFile.EndObject(); if (HostPlatform == UnrealTargetPlatform.Win64) { OutFile.AddField("console", "integratedTerminal"); } else { OutFile.AddField("console", "internalConsole"); OutFile.AddField("internalConsoleOptions", "openOnSessionStart"); } OutFile.AddField("stopAtEntry", false); OutFile.AddField("cwd", MakeUnquotedPathString(CWD, EPathType.Absolute)); } OutFile.EndObject(); } private void WriteCSharpLaunchConfig(ProjectData.Project InProject, JsonFile OutFile) { foreach (ProjectData.Target Target in InProject.Targets) { foreach (ProjectData.BuildProduct BuildProduct in Target.BuildProducts) { if (BuildProduct.OutputType == ProjectData.EOutputType.Exe) { string TaskName = String.Format("{0} ({1})", Target.Name, BuildProduct.Config); string BuildTaskName = String.Format("{0} {1} {2} Build", Target.Name, HostPlatform, BuildProduct.Config); string[]? Args = null; if (Target.Name.Equals("AutomationTool", StringComparison.OrdinalIgnoreCase)) { BuildTaskName = String.Format("{0}+Scripts {1} {2} Build", Target.Name, HostPlatform, BuildProduct.Config); Args = new string[] {"-NoCompile"}; } WriteSingleCSharpLaunchConfig(OutFile, TaskName, BuildTaskName, BuildProduct.OutputFile, Args); } } } } private void WriteLaunch(JsonFile OutFile, ProjectData ProjectData, ILogger Logger) { OutFile.AddField("version", "0.2.0"); if (bAddDebugCoreConfig) { OutFile.BeginArray("inputs"); OutFile.BeginObject(); OutFile.AddField("id", "coreFileName"); OutFile.AddField("type", "command"); OutFile.AddField("command", "filePicker.pick"); OutFile.BeginObject("args"); OutFile.AddField("masks", "core*"); OutFile.BeginObject("display"); OutFile.AddField("type", "fileRelativePath"); OutFile.AddField("detail", "filePath"); OutFile.EndObject(); OutFile.AddField("output", "filePath"); OutFile.EndObject(); OutFile.EndObject(); OutFile.EndArray(); } OutFile.BeginArray("configurations"); { if (!bUseVSCodeExtension) { foreach (ProjectData.Project Project in ProjectData.NativeProjects) { WriteNativeLaunchConfig(Project, OutFile, Logger); } } foreach (ProjectData.Project Project in ProjectData.CSharpProjects) { WriteCSharpLaunchConfig(Project, OutFile); } } // Add in a special task for regenerating project files string PreLaunchTask = ""; List Args = new List(); Args.Add("-projectfiles"); Args.Add("-vscode"); if (bGeneratingGameProjectFiles) { Args.Add("-project=" + MakeUnquotedPathString(OnlyGameProject!, EPathType.Absolute)); Args.Add("-game"); } if (bIncludeEngineSource) { Args.Add("-engine"); } if (bIncludeDotNetPrograms) { Args.Add("-dotnet"); PreLaunchTask = "UnrealBuildTool " + HostPlatform.ToString() + " Development Build"; } FileReference RunUbtPath = FileReference.Combine(ProjectRoot, "Engine", "Build", "BatchFiles", "RunUBT.bat"); WriteSingleCSharpLaunchConfig( OutFile, "Generate Project Files", PreLaunchTask, RunUbtPath, Args.ToArray() ); OutFile.EndArray(); } private void WriteWorkspaceIgnoreFile(List Projects) { List PathsToExclude = new List(); foreach (ProjectFile Project in Projects) { bool bFoundTarget = false; foreach (ProjectTarget Target in Project.ProjectTargets.OfType()) { if (Target.TargetFilePath != null) { DirectoryReference ProjDir = Target.TargetFilePath.Directory.GetDirectoryName() == "Source" ? Target.TargetFilePath.Directory.ParentDirectory! : Target.TargetFilePath.Directory; GetExcludePathsCPP(ProjDir, PathsToExclude); DirectoryReference PluginRootDir = DirectoryReference.Combine(ProjDir, "Plugins"); WriteWorkspaceIgnoreFileForPlugins(PluginRootDir, PathsToExclude); bFoundTarget = true; } } if (!bFoundTarget) { GetExcludePathsCSharp(Project.ProjectFilePath.Directory.ToString(), PathsToExclude); } } StringBuilder OutFile = new StringBuilder(); if (!IncludeAllFiles) { // TODO: Adding ignore patterns to .ignore hides files from Open File Dialog but it does not hide them in the File Explorer // but using files.exclude with our full set of excludes breaks vscode for larger code bases so a verbose file explorer // seems like less of an issue and thus we are not adding these to files.exclude. // see https://github.com/microsoft/vscode/issues/109380 for discussions with vscode team DirectoryReference WorkspaceRoot = bForeignProject ? Projects[0].BaseDir : Unreal.RootDirectory; string WorkspaceRootPath = WorkspaceRoot.ToString().Replace('\\', '/') + "/"; if (!bForeignProject) { OutFile.AppendLine(".vscode"); } foreach (string PathToExclude in PathsToExclude) { OutFile.AppendLine(PathToExclude.Replace('\\', '/').Replace(WorkspaceRootPath, "/")); } } FileReference.WriteAllText(FileReference.Combine(PrimaryProjectPath, ".ignore"), OutFile.ToString()); } private void WriteWorkspaceIgnoreFileForPlugins(DirectoryReference PluginBaseDir, List PathsToExclude) { if (DirectoryReference.Exists(PluginBaseDir)) { foreach (DirectoryReference SubDir in DirectoryReference.EnumerateDirectories(PluginBaseDir, "*", SearchOption.TopDirectoryOnly)) { string[] UPluginFiles = Directory.GetFiles(SubDir.ToString(), "*.uplugin"); if (UPluginFiles.Length == 1) { DirectoryReference PluginDir = SubDir; GetExcludePathsCPP(PluginDir, PathsToExclude); } else { WriteWorkspaceIgnoreFileForPlugins(SubDir, PathsToExclude); } } } } private void WriteWorkspaceFile(ProjectData ProjectData, ILogger Logger) { JsonFile WorkspaceFile = new JsonFile(); WorkspaceFile.BeginRootObject(); { WorkspaceFile.BeginArray("folders"); { // Add the directory in which which the code-workspace file exists. // This is also known as ${workspaceRoot} WorkspaceFile.BeginObject(); { string ProjectName = bForeignProject ? GameProjectName! : "UE5"; WorkspaceFile.AddField("name", ProjectName); WorkspaceFile.AddField("path", "."); } WorkspaceFile.EndObject(); // If this project is outside the engine folder, add the root engine directory if (bIncludeEngineSource && bForeignProject) { WorkspaceFile.BeginObject(); { WorkspaceFile.AddField("name", "UE5"); WorkspaceFile.AddField("path", MakeUnquotedPathString(Unreal.RootDirectory, EPathType.Absolute)); } WorkspaceFile.EndObject(); } } WorkspaceFile.EndArray(); } WorkspaceFile.BeginObject("settings"); { // disable autodetect for typescript files to workaround slowdown in vscode as a result of parsing all files WorkspaceFile.AddField("typescript.tsc.autoDetect", "off"); // disable npm script autodetect to avoid lag populating tasks list WorkspaceFile.AddField("npm.autoDetect", "off"); // set environment variables so correct dotnet is used WorkspaceFile.BeginObject($"terminal.integrated.env.{(OperatingSystem.IsWindows() ? "windows" : OperatingSystem.IsMacOS() ? "mac" : "linux")}"); { WorkspaceFile.AddField("PATH", $"{Unreal.DotnetDirectory}{Path.PathSeparator}${{env:PATH}}"); WorkspaceFile.AddField("DOTNET_ROOT", Unreal.DotnetDirectory.FullName); WorkspaceFile.AddField("DOTNET_HOST_PATH", Unreal.DotnetPath.FullName); WorkspaceFile.AddField("DOTNET_MULTILEVEL_LOOKUP", "0"); WorkspaceFile.AddField("DOTNET_ROLL_FORWARD", "LatestMajor"); } WorkspaceFile.EndObject(); if (bUseVSCodeExtension) { if (HostPlatform == UnrealTargetPlatform.Win64) { WorkspaceFile.AddField("UE.UBTScriptPath", MakePathString(FileReference.Combine(ProjectRoot, "Engine", "Build", "BatchFiles", "RunUBT.bat"), true)); } else { WorkspaceFile.AddField("UE.UBTScriptPath", MakePathString(FileReference.Combine(ProjectRoot, "Engine", "Build", "BatchFiles", HostPlatform.ToString(), "RunUBT.sh"), true)); } // Exclude some large directories/filetypes by default WorkspaceFile.BeginObject("files.exclude"); WorkspaceFile.AddField("**/Intermediate/", true); WorkspaceFile.AddField("**/Binaries/", true); WorkspaceFile.AddField("Engine/DerivedDataCache", true); WorkspaceFile.AddField("**/*.uasset", true); WorkspaceFile.AddField("**/*.umap", true); WorkspaceFile.AddField("**/*.uexp", true); WorkspaceFile.AddField("**/*.upayload", true); WorkspaceFile.AddField("**/*.ubulk", true); WorkspaceFile.AddField("**/*.m.ubulk", true); WorkspaceFile.AddField("**/*.uptnl", true); WorkspaceFile.EndObject(); WorkspaceFile.BeginObject("search.exclude"); WorkspaceFile.EndObject(); } } WorkspaceFile.EndObject(); WorkspaceFile.BeginObject("extensions"); { // extensions is a set of recommended extensions that a user should install. // Adding this section aids discovery of extensions which are helpful to have installed for Unreal development. WorkspaceFile.BeginArray("recommendations"); { // Add when/if published to marketplace. // WorkspaceFile.AddUnnamedField("epic.vscode-ue"); WorkspaceFile.AddUnnamedField("ms-vscode.cpptools"); WorkspaceFile.AddUnnamedField("ms-dotnettools.csharp"); if (bUseVSCodeExtension) { WorkspaceFile.AddUnnamedField("epic.vscode-ue"); } } WorkspaceFile.EndArray(); } WorkspaceFile.EndObject(); WorkspaceFile.BeginObject("tasks"); WriteTasks(WorkspaceFile, ProjectData); WorkspaceFile.EndObject(); WorkspaceFile.BeginObject("launch"); WriteLaunch(WorkspaceFile, ProjectData, Logger); WorkspaceFile.EndObject(); WorkspaceFile.EndRootObject(); string? WorkspaceName = bForeignProject ? GameProjectName : PrimaryProjectName; WorkspaceFile.Write(FileReference.Combine(PrimaryProjectPath, WorkspaceName + ".code-workspace")); } private void GetExcludePathsCPP(DirectoryReference BaseDir, List PathsToExclude) { string[] DirAllowList = { "Binaries", "Build", "Config", "Plugins", "Source", "Private", "Public", "Internal", "Classes", "Resources", "Restricted" }; foreach (DirectoryReference SubDir in DirectoryReference.EnumerateDirectories(BaseDir, "*", SearchOption.TopDirectoryOnly)) { if (Array.Find(DirAllowList, Dir => Dir == SubDir.GetDirectoryName()) == null) { string NewSubDir = SubDir.ToString(); if (!PathsToExclude.Contains(NewSubDir)) { PathsToExclude.Add(NewSubDir); } } } } private void GetExcludePathsCSharp(string BaseDir, List PathsToExclude) { string[] DenyList = { "obj", "bin" }; foreach (string DenyListDir in DenyList) { string ExcludePath = Path.Combine(BaseDir, DenyListDir); if (!PathsToExclude.Contains(ExcludePath)) { PathsToExclude.Add(ExcludePath); } } } } }