From 53e7e3f327d27608575225420cd23829acfcd69b Mon Sep 17 00:00:00 2001 From: Henrique Fernandes Baggio Date: Wed, 17 Jan 2024 19:49:54 -0800 Subject: [PATCH] [UE5.0] Project Generator for VS Workspace support Adding a new Project Generator for Visual Studio that will be used for the new support to opening `.uproject` files directly in the IDE without generating the traditional `.sln` and .vcxproj files. This is based on the QueryMode API in `ue5-main` and adapted to the GenerateProjectFiles flow. The project files will contain information for each Target+Configuration+Platform combinations that are used the project. Visual Studio will automatically invoke this new generator during the flow to open a `.uproject` file directly. --- .../Configuration/UEBuildTarget.cs | 2 +- .../Modes/GenerateProjectFilesMode.cs | 4 + .../Platform/Windows/VCToolChain.cs | 26 ++ .../ProjectFiles/ProjectFileGenerator.cs | 5 +- .../VSWorkspaceProjectFile.cs | 394 ++++++++++++++++++ .../VSWorkspaceProjectFileGenerator.cs | 248 +++++++++++ .../UnrealBuildTool/ToolChain/UEToolChain.cs | 20 + 7 files changed, 696 insertions(+), 3 deletions(-) create mode 100644 Engine/Source/Programs/UnrealBuildTool/ProjectFiles/VisualStudioWorkspace/VSWorkspaceProjectFile.cs create mode 100644 Engine/Source/Programs/UnrealBuildTool/ProjectFiles/VisualStudioWorkspace/VSWorkspaceProjectFileGenerator.cs diff --git a/Engine/Source/Programs/UnrealBuildTool/Configuration/UEBuildTarget.cs b/Engine/Source/Programs/UnrealBuildTool/Configuration/UEBuildTarget.cs index 25e98e1729cfb..04395105aaf64 100644 --- a/Engine/Source/Programs/UnrealBuildTool/Configuration/UEBuildTarget.cs +++ b/Engine/Source/Programs/UnrealBuildTool/Configuration/UEBuildTarget.cs @@ -2249,7 +2249,7 @@ List GetRestrictedFolders(FileReference File) /// Creates a toolchain for the current target. May be overridden by the target rules. /// /// New toolchain instance - private UEToolChain CreateToolchain(UnrealTargetPlatform Platform) + public UEToolChain CreateToolchain(UnrealTargetPlatform Platform) { if (Rules.ToolChainName == null) { diff --git a/Engine/Source/Programs/UnrealBuildTool/Modes/GenerateProjectFilesMode.cs b/Engine/Source/Programs/UnrealBuildTool/Modes/GenerateProjectFilesMode.cs index 5ac8f51d12f4f..a6e0cde49cfa2 100644 --- a/Engine/Source/Programs/UnrealBuildTool/Modes/GenerateProjectFilesMode.cs +++ b/Engine/Source/Programs/UnrealBuildTool/Modes/GenerateProjectFilesMode.cs @@ -35,6 +35,7 @@ class GenerateProjectFilesMode : ToolMode [CommandLine("-EddieProjectFiles", Value = nameof(ProjectFileFormat.Eddie))] [CommandLine("-VSCode", Value = nameof(ProjectFileFormat.VisualStudioCode))] [CommandLine("-VSMac", Value = nameof(ProjectFileFormat.VisualStudioMac))] + [CommandLine("-VSWorkspace", Value = nameof(ProjectFileFormat.VisualStudioWorkspace))] [CommandLine("-CLion", Value = nameof(ProjectFileFormat.CLion))] [CommandLine("-Rider", Value = nameof(ProjectFileFormat.Rider))] #if __VPROJECT_AVAILABLE__ @@ -202,6 +203,9 @@ public override int Execute(CommandLineArguments Arguments) case ProjectFileFormat.VisualStudioCode: Generator = new VSCodeProjectFileGenerator(ProjectFile); break; + case ProjectFileFormat.VisualStudioWorkspace: + Generator = new VSWorkspaceProjectFileGenerator(ProjectFile, Arguments); + break; case ProjectFileFormat.CLion: Generator = new CLionGenerator(ProjectFile); break; diff --git a/Engine/Source/Programs/UnrealBuildTool/Platform/Windows/VCToolChain.cs b/Engine/Source/Programs/UnrealBuildTool/Platform/Windows/VCToolChain.cs index 80ec89d70e37a..287e8abd3e78f 100644 --- a/Engine/Source/Programs/UnrealBuildTool/Platform/Windows/VCToolChain.cs +++ b/Engine/Source/Programs/UnrealBuildTool/Platform/Windows/VCToolChain.cs @@ -47,6 +47,11 @@ public VCToolChain(ReadOnlyTargetRules Target) } } + public override FileReference? GetCppCompilerPath() + { + return EnvVars.CompilerPath; + } + /// /// Prepares the environment for building /// @@ -209,6 +214,27 @@ public static void AddSourceDependsFile(List Arguments, FileItem SourceD Arguments.Add($"/clang:-MD /clang:-MF\"{SourceDependsFileString}\""); } + public override IEnumerable GetGlobalCommandLineArgs(CppCompileEnvironment CompileEnvironment) + { + List Arguments = new List(); + AppendCLArguments_Global(new CppCompileEnvironment(CompileEnvironment), Arguments); + return Arguments; + } + + public override IEnumerable GetCPPCommandLineArgs(CppCompileEnvironment CompileEnvironment) + { + List Arguments = new List(); + AppendCLArguments_CPP(CompileEnvironment, Arguments); + return Arguments; + } + + public override IEnumerable GetCCommandLineArgs(CppCompileEnvironment CompileEnvironment) + { + List Arguments = new List(); + AppendCLArguments_C(Arguments); + return Arguments; + } + protected virtual void AppendCLArguments_Global(CppCompileEnvironment CompileEnvironment, List Arguments) { // Suppress generation of object code for unreferenced inline functions. Enabling this option is more standards compliant, and causes a big reduction diff --git a/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/ProjectFileGenerator.cs b/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/ProjectFileGenerator.cs index 1e8e7e609c166..4276988ed1374 100644 --- a/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/ProjectFileGenerator.cs +++ b/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/ProjectFileGenerator.cs @@ -132,6 +132,7 @@ enum ProjectFileFormat VisualStudio, VisualStudio2019, VisualStudio2022, + VisualStudioWorkspace, XCode, Eddie, VisualStudioCode, @@ -2297,7 +2298,7 @@ private ProjectFile FindProjectForModule(FileReference CurModuleFile, ListMap of game folder name to all of the game projects we created /// Map of program names to all of the program projects we created /// Map of RuleAssemblies to their base folders - private void AddProjectsForAllTargets( + protected void AddProjectsForAllTargets( PlatformProjectGeneratorCollection PlatformProjectGenerators, List AllGames, List AllTargetFiles, @@ -2619,7 +2620,7 @@ protected void AddProjectsForMods(List GameProjects, out List /// The base name for the project file /// Full path to the project file - protected FileReference GetProjectLocation(string BaseName) + protected virtual FileReference GetProjectLocation(string BaseName) { return FileReference.Combine(IntermediateProjectFilesPath, BaseName + ProjectFileExtension); } diff --git a/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/VisualStudioWorkspace/VSWorkspaceProjectFile.cs b/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/VisualStudioWorkspace/VSWorkspaceProjectFile.cs new file mode 100644 index 0000000000000..05c5473a616db --- /dev/null +++ b/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/VisualStudioWorkspace/VSWorkspaceProjectFile.cs @@ -0,0 +1,394 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using EpicGames.Core; +using Microsoft.Extensions.Logging; +using UnrealBuildBase; + +namespace UnrealBuildTool +{ + internal class VSWorkspaceProjectFile : ProjectFile + { + private readonly DirectoryReference RootPath; + private readonly HashSet TargetTypes; + private readonly CommandLineArguments Arguments; + private VCProjectFileSettings Settings; + private bool bUsePrecompiled; + + /// + /// Collection of output files for this project + /// + public List ExportedTargetProjects { get; set; } = new List(); + + public VSWorkspaceProjectFile(FileReference InProjectFilePath, DirectoryReference BaseDir, + DirectoryReference RootPath, HashSet TargetTypes, CommandLineArguments Arguments, VCProjectFileSettings Settings, bool bUsePrecompiled) + : base(InProjectFilePath, BaseDir) + { + this.RootPath = RootPath; + this.TargetTypes = TargetTypes; + this.Arguments = Arguments; + this.Settings = Settings; + this.bUsePrecompiled = bUsePrecompiled; + } + + /// + /// Write project file info in JSON file. + /// For every combination of UnrealTargetPlatform, UnrealTargetConfiguration and TargetType + /// will be generated separate JSON file. + /// + public bool WriteProjectFile(List InPlatforms, + List InConfigurations, + PlatformProjectGeneratorCollection PlatformProjectGenerators, JsonWriterStyle Minimize) + { + DirectoryReference ProjectRootFolder = RootPath; + + Dictionary FileToTarget = new Dictionary(); + + foreach (UnrealTargetPlatform Platform in InPlatforms) + { + foreach (UnrealTargetConfiguration Configuration in InConfigurations) + { + foreach (ProjectTarget ProjectTarget in ProjectTargets.OfType()) + { + if (TargetTypes.Any() && !TargetTypes.Contains(ProjectTarget.TargetRules!.Type)) + { + continue; + } + + // Skip Programs for all configs except for current platform + Development & Debug configurations + if (ProjectTarget.TargetRules!.Type == TargetType.Program && + (BuildHostPlatform.Current.Platform != Platform || + !(Configuration == UnrealTargetConfiguration.Development || Configuration == UnrealTargetConfiguration.Debug))) + { + continue; + } + + // Skip Editor for all platforms except for current platform + if (ProjectTarget.TargetRules.Type == TargetType.Editor && + (BuildHostPlatform.Current.Platform != Platform || + (Configuration == UnrealTargetConfiguration.Test || Configuration == UnrealTargetConfiguration.Shipping))) + { + continue; + } + + bool bBuildByDefault = ShouldBuildByDefaultForSolutionTargets && ProjectTarget.SupportedPlatforms.Contains(Platform); + + string DefaultArchitecture = UEBuildPlatform + .GetBuildPlatform(Platform) + .GetDefaultArchitecture(ProjectTarget.UnrealProjectFilePath); + + TargetDescriptor TargetDesc = new TargetDescriptor(ProjectTarget.UnrealProjectFilePath, ProjectTarget.Name, + Platform, Configuration, DefaultArchitecture, Arguments); + + try + { + FileReference OutputFile = FileReference.Combine(ProjectRootFolder, + $"{ProjectTarget.TargetFilePath.GetFileNameWithoutAnyExtensions()}_{Configuration}_{Platform}.json"); + + UEBuildTarget BuildTarget = UEBuildTarget.Create(TargetDesc, false, false, false); + FileToTarget.Add(OutputFile, (BuildTarget, bBuildByDefault)); + } + catch (Exception Ex) + { + Log.TraceWarning("Exception while generating include data for Target:{Target}, Platform: {Platform}, Configuration: {Configuration}", TargetDesc.Name, Platform.ToString(), Configuration.ToString()); + Log.TraceWarning("{Ex}", Ex.ToString()); + } + } + } + } + + foreach (var Entry in FileToTarget) + { + var OutputFile = Entry.Key; + var BuildTarget = Entry.Value.BuildTarget; + var bBuildByDefault = Entry.Value.bBuildByDefault; + + try + { + BuildTarget.PreBuildSetup(); + + ExportedTargetInfo TargetInfo = ExportTarget(BuildTarget, bBuildByDefault, PlatformProjectGenerators); + + DirectoryReference.CreateDirectory(OutputFile.Directory); + + File.WriteAllText(OutputFile.FullName, JsonSerializer.Serialize(TargetInfo, options: new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = Minimize == JsonWriterStyle.Readable, + })); + + ExportedTargetProjects.Add(TargetInfo); + } + catch (Exception Ex) + { + Log.TraceWarning("Exception while generating include data for Target:{Target}, Platform: {Platform}, Configuration: {Configuration}", + BuildTarget.AppName, BuildTarget.Platform.ToString(), BuildTarget.Configuration.ToString()); + Log.TraceWarning("{Ex}", Ex.ToString()); + } + } + + return true; + } + + private ExportedTargetInfo ExportTarget( + UEBuildTarget Target, bool bBuildByDefault, PlatformProjectGeneratorCollection PlatformProjectGenerators) + { + ExportedTargetInfo TargetInfo = new ExportedTargetInfo() + { + TargetName = Target.TargetName, + TargetPath = Target.TargetRulesFile.FullName, + ProjectPath = Target.ProjectFile?.FullName ?? String.Empty, + TargetType = Target.TargetType.ToString(), + Platform = Target.Platform.ToString(), + Configuration = Target.Configuration.ToString(), + BuildInfo = ExportBuildInfo(Target, PlatformProjectGenerators, bBuildByDefault) + }; + + UEToolChain TargetToolChain = Target.CreateToolchain(Target.Platform); + CppCompileEnvironment GlobalCompileEnvironment = Target.CreateCompileEnvironmentForProjectFiles(); + + HashSet ModuleNames = new HashSet(); + foreach (UEBuildBinary Binary in Target.Binaries) + { + CppCompileEnvironment BinaryCompileEnvironment = Binary.CreateBinaryCompileEnvironment(GlobalCompileEnvironment); + IEnumerable CandidateModules = Binary.Modules.Where(x => x is UEBuildModuleCPP).Cast(); + + foreach (var ModuleCpp in CandidateModules) + { + if (!ModuleNames.Add(ModuleCpp.Name)) + { + continue; + } + + CppCompileEnvironment ModuleCompileEnvironment = ModuleCpp.CreateCompileEnvironmentForIntellisense(Target.Rules, BinaryCompileEnvironment); + TargetInfo.ModuleToCompileSettings.Add(ModuleCpp.Name, ExportModule(ModuleCpp, TargetToolChain, ModuleCompileEnvironment)); + + foreach (DirectoryReference ModuleDirectory in ModuleCpp.ModuleDirectories) + { + TargetInfo.DirToModule.TryAdd(ModuleDirectory.FullName, ModuleCpp.Name); + } + + if (ModuleCpp.GeneratedCodeDirectory != null) + { + TargetInfo.DirToModule.TryAdd(ModuleCpp.GeneratedCodeDirectory.FullName, ModuleCpp.Name); + } + } + } + + return TargetInfo; + } + + private static ExportedModuleInfo ExportModule(UEBuildModuleCPP Module, UEToolChain TargetToolChain, CppCompileEnvironment ModuleCompileEnvironment) + { + ExportedModuleInfo Result = new ExportedModuleInfo() + { + Name = Module.Name, + Directory = Module.ModuleDirectory.FullName, + Rules = Module.RulesFile.FullName, + GeneratedCodeDirectory = Module.GeneratedCodeDirectory != null ? Module.GeneratedCodeDirectory.FullName : String.Empty, + Standard = ModuleCompileEnvironment.CppStandard.ToString(), + }; + + Result.IncludePaths.AddRange(Module.PublicIncludePaths.Select(x => x.FullName)); + Result.IncludePaths.AddRange(Module.PublicSystemIncludePaths.Select(x => x.FullName)); + Result.IncludePaths.AddRange(Module.InternalIncludePaths.Select(x => x.FullName)); + Result.IncludePaths.AddRange(Module.LegacyPublicIncludePaths.Select(x => x.FullName)); + Result.IncludePaths.AddRange(Module.PrivateIncludePaths.Select(x => x.FullName)); + + Result.IncludePaths.AddRange(ModuleCompileEnvironment.UserIncludePaths.Select(x => x.FullName)); + + Result.IncludePaths.AddRange(Module.PublicSystemLibraryPaths.Select(x => x.FullName)); + Result.IncludePaths.AddRange(Module.PublicSystemLibraries.Concat(Module.PublicLibraries.Select(x => x.FullName))); + + if (TargetToolChain is VCToolChain TargetVCToolChain) + { + string VCIncludePaths = VCToolChain.GetVCIncludePaths(ModuleCompileEnvironment.Platform, WindowsCompiler.VisualStudio2022, null); + Result.IncludePaths.AddRange(VCIncludePaths.Split(";")); + } + + Result.Defines.AddRange(Module.PublicDefinitions); + Result.Defines.AddRange(Module.Rules.PrivateDefinitions); + Result.Defines.AddRange(Module.Rules.bTreatAsEngineModule ? Array.Empty() : Module.Rules.Target.ProjectDefinitions); + Result.Defines.AddRange(Module.GetEmptyApiMacros()); + + var ForcedIncludes = ModuleCompileEnvironment.ForceIncludeFiles.ToList(); + if (ModuleCompileEnvironment.PrecompiledHeaderAction == PrecompiledHeaderAction.Include) + { + FileItem IncludeHeader = FileItem.GetItemByFileReference(ModuleCompileEnvironment.PrecompiledHeaderIncludeFilename!); + ForcedIncludes.Insert(0, IncludeHeader); + } + + Result.ForcedIncludes.AddRange(ForcedIncludes.Select(x => x.FullName)); + + Result.CompilerPath = TargetToolChain.GetCppCompilerPath()?.ToString(); + Result.CompilerArgs.AddRange(TargetToolChain.GetGlobalCommandLineArgs(ModuleCompileEnvironment)); + Result.CompilerAdditionalArgs.Add("c", TargetToolChain.GetCCommandLineArgs(ModuleCompileEnvironment).ToList()); + Result.CompilerAdditionalArgs.Add("cpp", TargetToolChain.GetCPPCommandLineArgs(ModuleCompileEnvironment).ToList()); + + return Result; + } + + private TargetBuildInfo? ExportBuildInfo(UEBuildTarget Target, PlatformProjectGeneratorCollection PlatformProjectGenerators, bool bBuildByDefault) + { + if (!BuildHostPlatform.Current.Platform.IsInGroup(UnrealPlatformGroup.Windows)) + { + Log.TraceWarning("Unsupported platform for Build Information: {Platform}", BuildHostPlatform.Current.Platform.ToString()); + return null; + } + + if (IsStubProject) + { + return null; + } + + ProjectTarget ProjectTarget = ProjectTargets.OfType().Single(It => Target.TargetRulesFile == It.TargetFilePath); + UnrealTargetPlatform Platform = Target.Platform; + UnrealTargetConfiguration Configuration = Target.Configuration; + + string UProjectPath = ""; + if (IsForeignProject) + { + UProjectPath = String.Format("\"{0}\"", ProjectTarget.UnrealProjectFilePath!.FullName); + } + + PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(Platform, true); + + string BuildArguments = GetBuildArguments(ProjGenerator, ProjectTarget, Target, UProjectPath); + + return new TargetBuildInfo() + { + BuildCmd = $"{GetBuildScript("Build")} {BuildArguments}", + RebuildCmd = $"{GetBuildScript("Rebuild")} {BuildArguments}", + CleanCmd = $"{GetBuildScript("Clean")} {BuildArguments}", + PrimaryOutput = Target.Binaries[0].OutputFilePath.FullName, + BuildByDefault = bBuildByDefault, + }; + } + + private string GetBuildScript(string Cmd) + { + DirectoryReference BatchFilesDirectory = DirectoryReference.Combine(Unreal.EngineDirectory, "Build", "BatchFiles"); + return EscapePath(FileReference.Combine(BatchFilesDirectory, $"{Cmd}.bat").FullName); + } + + private string GetBuildArguments( + PlatformProjectGenerator? ProjGenerator, ProjectTarget ProjectTarget, UEBuildTarget Target, string UProjectPath) + { + UnrealTargetConfiguration Configuration = Target.Configuration; + UnrealTargetPlatform Platform = Target.Platform; + string TargetName = ProjectTarget.TargetFilePath.GetFileNameWithoutAnyExtensions(); + TargetRules TargetRulesObject = ProjectTarget.TargetRules!; + + // This is the standard UE based project NMake build line: + // ..\..\Build\BatchFiles\Build.bat + // ie ..\..\Build\BatchFiles\Build.bat BlankProgram Win64 Debug + + StringBuilder BuildArguments = new StringBuilder(); + + BuildArguments.AppendFormat("{0} {1} {2}", TargetName, Configuration, Platform); + if (IsForeignProject) + { + BuildArguments.AppendFormat(" -Project={0}", UProjectPath); + } + + List ExtraTargets = new List(); + if (!bUsePrecompiled) + { + if (TargetRulesObject.Type == TargetType.Editor && Settings.bEditorDependsOnShaderCompileWorker && !Unreal.IsEngineInstalled()) + { + ExtraTargets.Add("ShaderCompileWorker Win64 Development"); + } + if (TargetRulesObject.bWithLiveCoding && Settings.bBuildLiveCodingConsole && !Unreal.IsEngineInstalled() && TargetRulesObject.Name != "LiveCodingConsole") + { + ExtraTargets.Add(TargetRulesObject.bUseDebugLiveCodingConsole ? "LiveCodingConsole Win64 Debug" : "LiveCodingConsole Win64 Development"); + } + } + + if (ExtraTargets.Count > 0) + { + BuildArguments.Replace("\"", "\\\""); + BuildArguments.Insert(0, "-Target=\""); + BuildArguments.Append("\""); + foreach (string ExtraTarget in ExtraTargets) + { + BuildArguments.AppendFormat(" -Target=\"{0} -Quiet\"", ExtraTarget); + } + } + + if (bUsePrecompiled) + { + BuildArguments.Append(" -UsePrecompiled"); + } + + // Always wait for the mutex between UBT invocations, so that building the whole solution doesn't fail. + BuildArguments.Append(" -WaitMutex"); + + // Always include a flag to format log messages for MSBuild + BuildArguments.Append(" -FromMsBuild"); + + if (Settings.bAddFastPDBToProjects) + { + // Pass Fast PDB option to make use of Visual Studio's /DEBUG:FASTLINK option + BuildArguments.Append(" -FastPDB"); + } + + if (ProjGenerator != null) + { + BuildArguments.Append(ProjGenerator.GetExtraBuildArguments(Platform, Configuration)); + } + + return BuildArguments.ToString(); + } + + private static void WriteCommand(JsonWriter Writer, string CommandName, string Command, string Arguments) + { + Writer.WriteObjectStart(CommandName); + Writer.WriteValue("Command", Command); + Writer.WriteValue("Args", Arguments); + Writer.WriteObjectEnd(); + } + + internal class ExportedTargetInfo + { + public string TargetName { get; set; } = String.Empty; + public string TargetPath { get; set; } = String.Empty; + public string ProjectPath { get; set; } = String.Empty; + public string TargetType { get; set; } = String.Empty; + public string Platform { get; set; } = String.Empty; + public string Configuration { get; set; } = String.Empty; + public TargetBuildInfo? BuildInfo { get; set; } + public Dictionary ModuleToCompileSettings { get; set; } = new Dictionary(); + public Dictionary DirToModule { get; set; } = new Dictionary(); + } + + internal class ExportedModuleInfo + { + public string Name { get; set; } = String.Empty; + public string Directory { get; set; } = String.Empty; + public string Rules { get; set; } = String.Empty; + public string GeneratedCodeDirectory { get; set; } = String.Empty; + public List IncludePaths { get; set; } = new List(); + public List Defines { get; set; } = new List(); + public string? Standard { get; set; } + public List ForcedIncludes { get; set; } = new List(); + public string? CompilerPath { get; set; } + public List CompilerArgs { get; set; } = new List(); + public Dictionary> CompilerAdditionalArgs { get; set; } = new Dictionary>(); + public string? WindowsSdkVersion { get; set; } + } + + internal class TargetBuildInfo + { + public string BuildCmd { get; set; } = String.Empty; + public string RebuildCmd { get; set; } = String.Empty; + public string CleanCmd { get; set; } = String.Empty; + public string PrimaryOutput { get; set; } = String.Empty; + public bool BuildByDefault { get; internal set; } + } + } +} diff --git a/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/VisualStudioWorkspace/VSWorkspaceProjectFileGenerator.cs b/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/VisualStudioWorkspace/VSWorkspaceProjectFileGenerator.cs new file mode 100644 index 0000000000000..454e291171e00 --- /dev/null +++ b/Engine/Source/Programs/UnrealBuildTool/ProjectFiles/VisualStudioWorkspace/VSWorkspaceProjectFileGenerator.cs @@ -0,0 +1,248 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using EpicGames.Core; +using Microsoft.Extensions.Logging; +using UnrealBuildBase; + +namespace UnrealBuildTool +{ + class VSWorkspaceProjectFileGenerator : ProjectFileGenerator + { + public override string ProjectFileExtension => ".json"; + + // These properties are used by Visual Studio to determine where to read the project files. + // So they must remain constant. + private const string VSUnrealWorkspaceFileName = ".vs-unreal-workspace"; + private const string ProjectFilesFolder = "VisualStudio"; + + private readonly CommandLineArguments Arguments; + + /// + /// List of deprecated platforms. + /// Don't generate project model for these platforms unless they are specified in "Platforms" console arguments. + /// + /// + private readonly HashSet DeprecatedPlatforms = new HashSet(); + + /// + /// Platforms to generate project files for + /// + [CommandLine("-Platforms=", ListSeparator = '+')] + HashSet Platforms = new HashSet(); + + /// + /// Target types to generate project files for + /// + [CommandLine("-TargetTypes=", ListSeparator = '+')] + HashSet TargetTypes = new HashSet(); + + /// + /// Target configurations to generate project files for + /// + [CommandLine("-TargetConfigurations=", ListSeparator = '+')] + HashSet TargetConfigurations = new HashSet(); + + /// + /// Projects to generate project files for + /// + [CommandLine("-ProjectNames=", ListSeparator = '+')] + HashSet ProjectNames = new HashSet(); + + /// + /// Should format JSON files in human readable form, or use packed one without indents + /// + [CommandLine("-Minimize", Value = "Compact")] + private JsonWriterStyle Minimize = JsonWriterStyle.Readable; + + /// + /// The settings object + /// + protected VCProjectFileSettings Settings = new VCProjectFileSettings(); + + public VSWorkspaceProjectFileGenerator(FileReference? InOnlyGameProject, + CommandLineArguments InArguments) + : base(InOnlyGameProject) + { + Arguments = InArguments; + Arguments.ApplyTo(this); + XmlConfig.ApplyTo(Settings); + } + + public override bool ShouldGenerateIntelliSenseData() => true; + + public override void CleanProjectFiles(DirectoryReference InPrimaryProjectDirectory, string InPrimaryProjectName, + DirectoryReference InIntermediateProjectFilesDirectory) + { + DirectoryReference.Delete(InPrimaryProjectDirectory); + } + + protected override void ConfigureProjectFileGeneration(string[] Arguments, ref bool IncludeAllPlatforms) + { + base.ConfigureProjectFileGeneration(Arguments, ref IncludeAllPlatforms); + } + + protected override ProjectFile AllocateProjectFile(FileReference InitFilePath, DirectoryReference BaseDir) + { + VSWorkspaceProjectFile projectFile = new VSWorkspaceProjectFile(InitFilePath, BaseDir, InitFilePath.Directory, + TargetTypes, Arguments, Settings, bUsePrecompiled); + return projectFile; + } + + protected override bool WriteProjectFiles(PlatformProjectGeneratorCollection PlatformProjectGenerators) + { + using ProgressWriter Progress = new ProgressWriter("Writing project files...", true); + List ProjectsToGenerate = new List(GeneratedProjectFiles); + if (ProjectNames.Any()) + { + ProjectsToGenerate = ProjectsToGenerate.Where(Project => + ProjectNames.Contains(Project.ProjectFilePath.GetFileNameWithoutAnyExtensions())).ToList(); + } + + int TotalProjectFileCount = ProjectsToGenerate.Count; + + HashSet PlatformsToGenerate = new HashSet(SupportedPlatforms); + if (Platforms.Any()) + { + PlatformsToGenerate.IntersectWith(Platforms); + } + + List FilteredPlatforms = PlatformsToGenerate.Where(Platform => + { + // Skip deprecated unless explicitly specified in the command line. + return (!DeprecatedPlatforms.Contains(Platform) || Platforms.Contains(Platform)) + && UEBuildPlatform.IsPlatformAvailable(Platform); + }).ToList(); + + HashSet ConfigurationsToGenerate = new HashSet(SupportedConfigurations); + if (TargetConfigurations.Any()) + { + ConfigurationsToGenerate.IntersectWith(TargetConfigurations); + } + + List Configurations = ConfigurationsToGenerate.ToList(); + + for (int ProjectFileIndex = 0; ProjectFileIndex < ProjectsToGenerate.Count; ++ProjectFileIndex) + { + VSWorkspaceProjectFile? CurrentProject = ProjectsToGenerate[ProjectFileIndex] as VSWorkspaceProjectFile; + if (CurrentProject == null) + { + return false; + } + + if (!CurrentProject.WriteProjectFile(FilteredPlatforms, Configurations, PlatformProjectGenerators, Minimize)) + { + return false; + } + + Progress.Write(ProjectFileIndex + 1, TotalProjectFileCount); + } + + Progress.Write(TotalProjectFileCount, TotalProjectFileCount); + + return true; + } + + public override bool GenerateProjectFiles(PlatformProjectGeneratorCollection PlatformProjectGenerators, + String[] Arguments) + { + bool IncludeAllPlatforms = true; + ConfigureProjectFileGeneration(Arguments, ref IncludeAllPlatforms); + + if (bGeneratingGameProjectFiles || Unreal.IsEngineInstalled()) + { + PrimaryProjectPath = OnlyGameProject!.Directory; + PrimaryProjectName = OnlyGameProject.GetFileNameWithoutExtension(); + + IntermediateProjectFilesPath = + DirectoryReference.Combine(PrimaryProjectPath, "Intermediate", "ProjectFiles"); + } + + SetupSupportedPlatformsAndConfigurations(IncludeAllPlatforms: true, out string SupportedPlatformNames); + Log.TraceVerbose("Supported platforms: {Platforms}", SupportedPlatformNames); + + List AllGames = FindGameProjects(); + + { + // Find all of the target files. + List AllTargetFiles = DiscoverTargets(AllGames); + + // If there are multiple targets of a given type for a project, use the order to determine which one gets a suffix. + AllTargetFiles = AllTargetFiles.OrderBy(x => x.FullName, StringComparer.OrdinalIgnoreCase).ToList(); + + AddProjectsForAllTargets( + PlatformProjectGenerators, + AllGames, + AllTargetFiles, + Arguments, + out ProjectFile? EngineProject, + out List? GameProjects, + out Dictionary? ProgramProjects, + out Dictionary? RulesAssemblies); + + AddAllGameProjects(GameProjects); + } + + WriteProjectFiles(PlatformProjectGenerators); + WritePrimaryProjectFile(UBTProject, PlatformProjectGenerators); + + return true; + } + + protected override bool WritePrimaryProjectFile(ProjectFile? UBTProject, + PlatformProjectGeneratorCollection PlatformProjectGenerators) + { + try + { + FileReference PrimaryProjectFile = FileReference.Combine( + IntermediateProjectFilesPath, ProjectFilesFolder, VSUnrealWorkspaceFileName); + + DirectoryReference.CreateDirectory(PrimaryProjectFile.Directory); + + // Collect all the resulting project files and aggregate the target-level data + var AggregatedProjectInfo = GeneratedProjectFiles + .Where(Project => Project is VSWorkspaceProjectFile) + .OfType() + .SelectMany(Project => Project.ExportedTargetProjects) + .GroupBy(TargetProject => TargetProject.TargetName) + .Select(g => (g.Key, Target: new + { + TargetType = g.Select(i => i.TargetType).Distinct().Single(), + TargetPath = g.Select(i => i.TargetPath).Distinct().Single(), + ProjectPath = g.Select(i => i.ProjectPath).Distinct().Single(), + Configurations = g.Select(i => i.Configuration).Distinct().ToList(), + Platforms = g.Select(i => i.Platform).Distinct().ToList(), + })); + + // The inner Targets object is needed for schema compatibility with the Query Mode API. + var Result = new + { + Targets = AggregatedProjectInfo.ToDictionary(item => item.Key, item => item.Target) + }; + + File.WriteAllText(PrimaryProjectFile.FullName, JsonSerializer.Serialize(Result, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = Minimize == JsonWriterStyle.Readable, + })); + } + catch (Exception Ex) + { + Log.TraceWarning("Exception while writing root project file: {Ex}", Ex.ToString()); + return false; + } + + return true; + } + + /// + protected override FileReference GetProjectLocation(string BaseName) + { + return FileReference.Combine(IntermediateProjectFilesPath, ProjectFilesFolder, BaseName + ProjectFileExtension); + } + } +} diff --git a/Engine/Source/Programs/UnrealBuildTool/ToolChain/UEToolChain.cs b/Engine/Source/Programs/UnrealBuildTool/ToolChain/UEToolChain.cs index e8b40fff208d9..4f4fff19c8551 100644 --- a/Engine/Source/Programs/UnrealBuildTool/ToolChain/UEToolChain.cs +++ b/Engine/Source/Programs/UnrealBuildTool/ToolChain/UEToolChain.cs @@ -34,6 +34,12 @@ public static DirectoryReference GetModuleInterfaceDir(DirectoryReference Output return DirectoryReference.Combine(OutputDir, "Ifc"); } + // Return the path to the cpp compiler that will be used by this toolchain. + public virtual FileReference? GetCppCompilerPath() + { + return null; + } + public abstract CPPOutput CompileCPPFiles(CppCompileEnvironment CompileEnvironment, List InputFiles, DirectoryReference OutputDir, string ModuleName, IActionGraphBuilder Graph); public virtual CPPOutput CompileRCFiles(CppCompileEnvironment Environment, List InputFiles, DirectoryReference OutputDir, IActionGraphBuilder Graph) @@ -65,6 +71,20 @@ public virtual FileItem[] LinkAllFiles(LinkEnvironment LinkEnvironment, bool bBu return LinkFile != null ? new FileItem[] { LinkFile } : new FileItem[] { }; } + public virtual IEnumerable GetGlobalCommandLineArgs(CppCompileEnvironment CompileEnvironment) + { + return Array.Empty(); + } + + public virtual IEnumerable GetCPPCommandLineArgs(CppCompileEnvironment CompileEnvironment) + { + return Array.Empty(); + } + + public virtual IEnumerable GetCCommandLineArgs(CppCompileEnvironment CompileEnvironment) + { + return Array.Empty(); + } /// /// Get the name of the response file for the current linker environment and output file