// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Security; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using UnrealBuildBase; using static UnrealBuildTool.PlatformProjectGenerator; namespace UnrealBuildTool { /// /// Specifies the context for building an MSBuild project /// class MSBuildProjectContext { /// /// Name of the configuration /// public string ConfigurationName; /// /// Name of the platform /// public string PlatformName; /// /// Whether this context should be built by default /// public bool bBuildByDefault; /// /// Whether this context should be deployed by default /// public bool bDeployByDefault; /// /// Constructor /// /// Name of this configuration /// Name of this platform public MSBuildProjectContext(string ConfigurationName, string PlatformName) { this.ConfigurationName = ConfigurationName; this.PlatformName = PlatformName; } /// /// The name of this context /// public string Name => String.Format("{0}|{1}", ConfigurationName, PlatformName); /// /// Serializes this context to a string for debugging /// /// Name of this context public override string ToString() { return Name; } } /// /// Represents an arbitrary MSBuild project /// abstract class MSBuildProjectFile : ProjectFile { /// The project file version string public static readonly string VCProjectFileVersionString = "10.0.30319.1"; /// The build configuration name to use for stub project configurations. These are projects whose purpose /// is to make it easier for developers to find source files and to provide IntelliSense data for the module /// to Visual Studio public static readonly string StubProjectConfigurationName = "BuiltWithUnrealBuildTool"; /// The name of the Visual C++ platform to use for stub project configurations /// NOTE: We always use Win32 for the stub project's platform, since that is guaranteed to be supported by Visual Studio public static readonly string StubProjectPlatformName = "Win64"; /// /// The Guid representing the project type e.g. C# or C++ /// public virtual string ProjectTypeGUID => throw new BuildException("Unrecognized type of project file for Visual Studio solution"); static Guid PathNamespaceGuid { get; } = new Guid("2D8570D5-7FFC-4E6D-A9D7-E860E117D717"); /// /// Constructs a new project file object /// /// The path to the project file on disk /// The base directory for files within this project public MSBuildProjectFile(FileReference InitFilePath, DirectoryReference BaseDir) : base(InitFilePath, BaseDir) { // Each project gets its own GUID. This is stored in the project file and referenced in the solution file. // First, check to see if we have an existing file on disk. If we do, then we'll try to preserve the // GUID by loading it from the existing file. if (FileReference.Exists(ProjectFilePath)) { try { LoadGUIDFromExistingProject(); } catch (Exception) { // Failed to find GUID, so just create a new one ProjectGUID = VCProjectFileGenerator.MakeMd5Guid(PathNamespaceGuid, ProjectFilePath.FullName); } } if (ProjectGUID == Guid.Empty) { // Generate a brand new GUID ProjectGUID = VCProjectFileGenerator.MakeMd5Guid(PathNamespaceGuid, ProjectFilePath.FullName); } } /// /// Attempts to load the project's GUID from an existing project file on disk /// public override void LoadGUIDFromExistingProject() { // Only load GUIDs if we're in project generation mode. Regular builds don't need GUIDs for anything. if (ProjectFileGenerator.bGenerateProjectFiles) { XmlDocument Doc = new XmlDocument(); Doc.Load(ProjectFilePath.FullName); // @todo projectfiles: Ideally we could do a better job about preserving GUIDs when only minor changes are made // to the project (such as adding a single new file.) It would make diffing changes much easier! // @todo projectfiles: Can we "seed" a GUID based off the project path and generate consistent GUIDs each time? XmlNodeList Elements = Doc.GetElementsByTagName("ProjectGuid"); foreach (XmlElement? Element in Elements) { if (Element == null) { continue; } ProjectGUID = Guid.ParseExact(Element.InnerText.Trim("{}".ToCharArray()), "D"); } } } /// /// Get the project context for the given solution context /// /// The solution target type /// The solution configuration /// The solution platform /// Set of platform project generators /// The target architecture /// Logger for output /// Project context matching the given solution context public abstract MSBuildProjectContext? GetMatchingProjectContext(TargetType SolutionTarget, UnrealTargetConfiguration SolutionConfiguration, UnrealTargetPlatform SolutionPlatform, PlatformProjectGeneratorCollection PlatformProjectGenerators, UnrealArch? Architecture, ILogger Logger); /// /// Checks to see if the specified solution platform and configuration is able to map to this project /// /// The target that we're checking for a valid platform/config combination /// Platform /// Configuration /// Logger for output /// True if this is a valid combination for this project, otherwise false public static bool IsValidProjectPlatformAndConfiguration(Project ProjectTarget, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, ILogger Logger) { if (!ProjectFileGenerator.bIncludeTestAndShippingConfigs) { if (Configuration == UnrealTargetConfiguration.Test || Configuration == UnrealTargetConfiguration.Shipping) { return false; } } if (!ProjectFileGenerator.bIncludeDebugConfigs) { if (Configuration == UnrealTargetConfiguration.Debug || Configuration == UnrealTargetConfiguration.DebugGame) { return false; } } if (!ProjectFileGenerator.bIncludeDevelopmentConfigs) { if (Configuration == UnrealTargetConfiguration.Development) { return false; } } UEBuildPlatform? BuildPlatform; if (!UEBuildPlatform.TryGetBuildPlatform(Platform, out BuildPlatform)) { return false; } if (BuildPlatform.HasRequiredSDKsInstalled() != SDKStatus.Valid) { return false; } List SupportedConfigurations = new List(); List SupportedPlatforms = new List(); if (ProjectTarget.TargetRules != null) { SupportedPlatforms.AddRange(ProjectTarget.SupportedPlatforms); SupportedConfigurations.AddRange(ProjectTarget.TargetRules.GetSupportedConfigurations()); } // Add all of the extra platforms/configurations for this target { foreach (UnrealTargetPlatform ExtraPlatform in ProjectTarget.ExtraSupportedPlatforms) { if (!SupportedPlatforms.Contains(ExtraPlatform)) { SupportedPlatforms.Add(ExtraPlatform); } } foreach (UnrealTargetConfiguration ExtraConfig in ProjectTarget.ExtraSupportedConfigurations) { if (!SupportedConfigurations.Contains(ExtraConfig)) { SupportedConfigurations.Add(ExtraConfig); } } } // Only build for supported platforms if (SupportedPlatforms.Contains(Platform) == false) { return false; } // Only build for supported configurations if (SupportedConfigurations.Contains(Configuration) == false) { return false; } return true; } /// /// Escapes characters in a filename so they can be stored in an XML attribute /// /// The filename to escape /// The escaped filename public static string EscapeFileName(string FileName) { return SecurityElement.Escape(FileName)!; } /// /// GUID for this Visual C++ project file /// public Guid ProjectGUID { get; protected set; } } class VCProjectFile : MSBuildProjectFile { VCProjectFileFormat ProjectFileFormat; bool bUsePrecompiled; bool bMakeProjectPerTarget; string? BuildToolOverride; Dictionary ModuleDirToForceIncludePaths = new Dictionary(); Dictionary ModuleDirToPchHeaderFile = new Dictionary(); VCProjectFileSettings Settings; /// This is the platform name that Visual Studio is always guaranteed to support. We'll use this as /// a platform for any project configurations where our actual platform is not supported by the /// installed version of Visual Studio (e.g, "iOS") public const string DefaultPlatformName = "x64"; // This is the GUID that Visual Studio uses to identify a C++ project file in the solution public override string ProjectTypeGUID => "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"; /// /// Constructs a new project file object /// /// The path to the project file on disk /// The base directory for files within this project /// Visual C++ project file version /// Whether to add the -UsePrecompiled argumemnt when building targets /// Whether to add roll the target type into the config (ie "Development Editor") /// Optional arguments to pass to UBT when building /// Other settings public VCProjectFile(FileReference FilePath, DirectoryReference BaseDir, VCProjectFileFormat ProjectFileFormat, bool bUsePrecompiled, bool bMakeProjectPerTarget, string? BuildToolOverride, VCProjectFileSettings Settings) : base(FilePath, BaseDir) { this.ProjectFileFormat = ProjectFileFormat; this.bUsePrecompiled = bUsePrecompiled; this.bMakeProjectPerTarget = bMakeProjectPerTarget; this.BuildToolOverride = BuildToolOverride; this.Settings = Settings; } /// public override MSBuildProjectContext? GetMatchingProjectContext(TargetType SolutionTarget, UnrealTargetConfiguration SolutionConfiguration, UnrealTargetPlatform SolutionPlatform, PlatformProjectGeneratorCollection PlatformProjectGenerators, UnrealArch? Architecture, ILogger Logger) { // Stub projects always build in the same configuration if (IsStubProject) { return new MSBuildProjectContext(StubProjectConfigurationName, StubProjectPlatformName); } // Have to match every solution configuration combination to a project configuration (or use the invalid one) string ProjectConfigurationName = "Invalid"; // Get the default platform. If there were not valid platforms for this project, just use one that will always be available in VS. string ProjectPlatformName = InvalidConfigPlatformNames!.First(); // Whether the configuration should be built automatically as part of the solution bool bBuildByDefault = false; // Whether this configuration should deploy by default (requires bBuildByDefault) bool bDeployByDefault = false; // Programs are built in editor configurations (since the editor is like a desktop program too) and game configurations (since we omit the "game" qualification in the configuration name). bool IsProgramProject = ProjectTargets[0].TargetRules != null && ProjectTargets[0].TargetRules!.Type == TargetType.Program; if (!IsProgramProject || SolutionTarget == TargetType.Game || SolutionTarget == TargetType.Editor) { // Get the target type we expect to find for this project TargetType TargetConfigurationName = SolutionTarget; if (IsProgramProject) { TargetConfigurationName = TargetType.Program; } // Now, we want to find a target in this project that maps to the current solution config combination. Only up to one target should // and every solution config combination should map to at least one target in one project (otherwise we shouldn't have added it!). List MatchingProjectTargets = new List(); foreach (Project ProjectTarget in ProjectTargets) { if (VCProjectFile.IsValidProjectPlatformAndConfiguration(ProjectTarget, SolutionPlatform, SolutionConfiguration, Logger)) { if (ProjectTarget.TargetRules != null) { if (bMakeProjectPerTarget || TargetConfigurationName == ProjectTarget.TargetRules.Type) { MatchingProjectTargets.Add(ProjectTarget); } } else { if (ShouldBuildForAllSolutionTargets || TargetConfigurationName == TargetType.Game) { MatchingProjectTargets.Add(ProjectTarget); } } } } // Always allow SCW and UnrealLighmass to build in editor configurations if (MatchingProjectTargets.Count == 0 && SolutionTarget == TargetType.Editor && SolutionPlatform == UnrealTargetPlatform.Win64) { foreach (Project ProjectTarget in ProjectTargets) { string TargetName = ProjectTargets[0].TargetRules!.Name; if (TargetName == "ShaderCompileWorker") { MatchingProjectTargets.Add(ProjectTarget); break; } } } // Make sure there's only one matching project target if (MatchingProjectTargets.Count > 1) { throw new BuildException("Not expecting more than one target for project {0} to match solution configuration {1} {2} {3}", ProjectFilePath, SolutionTarget, SolutionConfiguration, SolutionPlatform); } // If we found a matching project, get matching configuration if (MatchingProjectTargets.Count == 1) { // Get the matching target Project MatchingProjectTarget = MatchingProjectTargets[0]; // If the project wants to always build in "Development", regardless of what the solution configuration is set to, then we'll do that here. UnrealTargetConfiguration ProjectConfiguration = SolutionConfiguration; if (MatchingProjectTarget.ForceDevelopmentConfiguration && SolutionTarget != TargetType.Game) { ProjectConfiguration = UnrealTargetConfiguration.Development; } // Get the matching project configuration UnrealTargetPlatform ProjectPlatform = SolutionPlatform; if (IsStubProject) { if (ProjectConfiguration != UnrealTargetConfiguration.Unknown) { throw new BuildException("Stub project was expecting platform and configuration type to be set to Unknown"); } ProjectConfigurationName = StubProjectConfigurationName; ProjectPlatformName = StubProjectPlatformName; } else { if (ProjectConfigAndTargetCombinations == null) { throw new BuildException("Project config and target combinations has not been populated."); } ProjectConfigAndTargetCombination? Combination = ProjectConfigAndTargetCombinations.FirstOrDefault(Combination => Combination.Platform == ProjectPlatform && Combination.Configuration == ProjectConfiguration && Combination.ProjectTarget == MatchingProjectTarget && Combination.Architecture == Architecture); if (Combination == null) { if (ProjectPlatform == UnrealTargetPlatform.Android) { // ok for Android not to find the architecture return null; } throw new BuildException("Could not find the project config/platform combination in the generated list."); } ProjectPlatformName = Combination.ProjectPlatformName; ProjectConfigurationName = Combination.ProjectConfigurationName; } // Set whether this project configuration should be built when the user initiates "build solution" if (ShouldBuildByDefaultForSolutionTargets) { // Some targets are "dummy targets"; they only exist to show user friendly errors in VS. Weed them out here, and don't set them to build by default. List? SupportedPlatforms = null; if (MatchingProjectTarget.TargetRules != null) { SupportedPlatforms = new List(); SupportedPlatforms.AddRange(MatchingProjectTarget.SupportedPlatforms); } if (SupportedPlatforms == null || SupportedPlatforms.Contains(SolutionPlatform)) { bBuildByDefault = true; PlatformProjectGenerator? ProjGen = PlatformProjectGenerators.GetPlatformProjectGenerator(SolutionPlatform, true); if (MatchingProjectTarget.ProjectDeploys || ((ProjGen != null) && (ProjGen.GetVisualStudioDeploymentEnabled(new VSSettings(ProjectPlatform, ProjectConfiguration, ProjectFileFormat, Architecture)) == true))) { bDeployByDefault = true; } } } } } return new MSBuildProjectContext(ProjectConfigurationName, ProjectPlatformName) { bBuildByDefault = bBuildByDefault, bDeployByDefault = bDeployByDefault }; } class ProjectConfigAndTargetCombination { readonly public UnrealTargetPlatform? Platform; readonly public UnrealTargetConfiguration Configuration; readonly public string ProjectPlatformName; readonly public string ProjectConfigurationName; readonly public ProjectTarget? ProjectTarget; readonly public UnrealArch? Architecture; public ProjectConfigAndTargetCombination(UnrealTargetPlatform? InPlatform, UnrealTargetConfiguration InConfiguration, string InProjectPlatformName, string InProjectConfigurationName, ProjectTarget? InProjectTarget, UnrealArch? InArchitecture) { Platform = InPlatform; Configuration = InConfiguration; ProjectPlatformName = InProjectPlatformName; ProjectConfigurationName = InProjectConfigurationName; ProjectTarget = InProjectTarget; Architecture = InArchitecture; } public string? ProjectConfigurationAndPlatformName => (ProjectPlatformName == null) ? null : (ProjectConfigurationName + "|" + ProjectPlatformName); public override string ToString() { return String.Format("{0} {1} {2}", ProjectTarget, Platform, Configuration, Architecture != null ? " " + Architecture : String.Empty); } } /// public override void AddModuleForIntelliSense(UEBuildModuleCPP Module, CppCompileEnvironment CompileEnvironment) { base.AddModuleForIntelliSense(Module, CompileEnvironment); if (Settings.bUsePerFileIntellisense) { foreach (DirectoryReference ModuleDirectory in Module.ModuleDirectories) { List ForceIncludePaths = new List(CompileEnvironment.ForceIncludeFiles.Select(x => InsertPathVariables(x.Location))); if (CompileEnvironment.PrecompiledHeaderIncludeFilename != null) { string PchHeaderFile = InsertPathVariables(CompileEnvironment.PrecompiledHeaderIncludeFilename); ForceIncludePaths.Insert(0, PchHeaderFile); ModuleDirToPchHeaderFile[Module.ModuleDirectory] = PchHeaderFile; } ModuleDirToForceIncludePaths[Module.ModuleDirectory] = String.Join(";", ForceIncludePaths); } } } static string InsertPathVariables(FileReference Location) { if (Location.IsUnderDirectory(ProjectFileGenerator.PrimaryProjectPath)) { return String.Format("$(SolutionDir){0}", Location.MakeRelativeTo(ProjectFileGenerator.PrimaryProjectPath)); } else { return Location.FullName; } } /// /// Gets highest C++ version which is used in this project /// /// C++ standard version public CppStandardVersion GetIntelliSenseCppVersion() { if (IntelliSenseCppVersion != CppStandardVersion.Default) { return IntelliSenseCppVersion; } CppStandardVersion Version = CppStandardVersion.Default; foreach (ProjectConfigAndTargetCombination Combination in ProjectConfigAndTargetCombinations!) { if (Combination.ProjectTarget != null && Combination.ProjectTarget.TargetRules != null && Combination.ProjectTarget.TargetRules.CppStandard > Version) { Version = Combination.ProjectTarget.TargetRules.CppStandard; } } return Version; } /// /// Gets compiler switch for specifying in AdditionalOptions in .vcxproj file for specific C++ version /// private static string GetCppStandardCompileArgument(CppStandardVersion Version) { switch (Version) { case CppStandardVersion.Cpp20: return "/std:c++20"; case CppStandardVersion.Cpp23: case CppStandardVersion.Latest: return "/std:c++latest"; default: throw new BuildException($"Unsupported C++ standard type set: {Version}"); } } private string GetConformanceCompileArguments(TargetRules Target) { if (Target.Platform.IsInGroup(UnrealPlatformGroup.Microsoft) && Target.WindowsPlatform.Compiler.IsMSVC()) { VersionNumber CompilerVersion = Target.WindowsPlatform.Environment?.CompilerVersion ?? new VersionNumber(0); List Arguments = new(); if (Target.WindowsPlatform.bStrictConformanceMode) { // This define is needed to ensure that MSVC static analysis mode doesn't declare attributes that are incompatible with strict conformance mode Arguments.Add("/DSAL_NO_ATTRIBUTE_DECLARATIONS=1"); Arguments.Add("/permissive-"); Arguments.Add("/Zc:strictStrings-"); // Have to disable strict const char* semantics due to Windows headers not being compliant. } else { Arguments.Add("/Zc:hiddenFriend"); } if (Target.WindowsPlatform.bUpdatedCPPMacro) { Arguments.Add("/Zc:__cplusplus"); } if (Target.WindowsPlatform.bStrictInlineConformance) { Arguments.Add("/Zc:inline"); } if (Target.WindowsPlatform.bStrictPreprocessorConformance) { Arguments.Add("/Zc:preprocessor"); } if (Target.WindowsPlatform.bStrictEnumTypesConformance) { Arguments.Add("/Zc:enumTypes"); } return String.Join(' ', Arguments); } return String.Empty; } /// /// Gets compiler switch for specifying in AdditionalOptions in .vcxproj file for coroutines support /// private string GetEnableCoroutinesArgument() { if (IntelliSenseEnableCoroutines) { if (VCProjectFileGenerator.GetCompilerForIntellisense(ProjectFileFormat).IsMSVC()) { return "/await:strict"; } return "-fcoroutines-ts"; } return String.Empty; } HashSet? InvalidConfigPlatformNames; List? ProjectConfigAndTargetCombinations; private void BuildProjectConfigAndTargetCombinations(List InPlatforms, List InConfigurations, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger) { //no need to do this more than once if (ProjectConfigAndTargetCombinations == null) { HashSet ProjectConfigAndTargets = new(); // Build up a list of platforms and configurations this project will support. In this list, Unknown simply // means that we should use the default "stub" project platform and configuration name. // If this is a "stub" project, then only add a single configuration to the project ProjectConfigAndTargetCombinations = new List(); if (IsStubProject) { ProjectConfigAndTargetCombination StubCombination = new ProjectConfigAndTargetCombination(UnrealTargetPlatform.Parse(StubProjectPlatformName), UnrealTargetConfiguration.Unknown, StubProjectPlatformName, StubProjectConfigurationName, null, null); ProjectConfigAndTargetCombinations.Add(StubCombination); } else { // Figure out all the desired configurations foreach (UnrealTargetConfiguration Configuration in InConfigurations) { //@todo.Rocket: Put this in a commonly accessible place? if (InstalledPlatformInfo.IsValidConfiguration(Configuration, EProjectType.Code) == false) { continue; } foreach (UnrealTargetPlatform Platform in InPlatforms) { if (InstalledPlatformInfo.IsValidPlatform(Platform, EProjectType.Code) == false) { continue; } UEBuildPlatform? BuildPlatform; if (UEBuildPlatform.TryGetBuildPlatform(Platform, out BuildPlatform) && (BuildPlatform.HasRequiredSDKsInstalled() == SDKStatus.Valid)) { // Now go through all of the target types for this project if (ProjectTargets.Count == 0) { throw new BuildException("Expecting at least one ProjectTarget to be associated with project '{0}' in the TargetProjects list ", ProjectFilePath); } foreach (ProjectTarget ProjectTarget in ProjectTargets.OfType()) { if (!IsValidProjectPlatformAndConfiguration(ProjectTarget, Platform, Configuration, Logger)) { continue; } Action AddProjectAndTargetCombination = (UnrealArch? Arch) => { PlatformProjectGenerator? PlatformProjectGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(Platform, bInAllowFailure: true); string ProjectPlatformName; string ProjectConfigurationName = Configuration.ToString(); bool CreateDistinctConfigName = false; VSSettings VSSettings = new(Platform, Configuration, ProjectFileFormat, Arch); // Check to see if this platform is supported directly by Visual Studio projects. if (PlatformProjectGenerator != null && PlatformProjectGenerator.HasVisualStudioSupport(VSSettings)) { // Allow the platform to specify the name used in VisualStudio. // Note that the actual name of the platform on the Visual Studio side may be different than what // UnrealBuildTool calls it (e.g. "Win64" -> "x64".) GetVisualStudioPlatformName() will figure this out. ProjectPlatformName = PlatformProjectGenerator.GetVisualStudioPlatformName(VSSettings); // The project generator may require a distinct configuration name - typically when two UnrealTargetPlatforms need the same ProjectPlatformName - otherwise the properties overwrite each oter. if (PlatformProjectGenerator.RequiresDistinctVisualStudioConfigurationName(VSSettings)) { CreateDistinctConfigName = true; } } else { // Visual Studio doesn't natively support this platform, so we fake it by mapping it to // a project configuration that has the platform name in that configuration as a suffix, // and then using "x64" as the actual VS platform name CreateDistinctConfigName = true; ProjectPlatformName = DefaultPlatformName; } if (CreateDistinctConfigName) { ProjectConfigurationName = String.Format("{0}{1}_{2}", Platform.ToString(), Arch != null ? "_" + Arch.ToString() : String.Empty, Configuration.ToString()); } TargetType TargetConfigurationType = ProjectTarget.TargetRules!.Type; if (!bMakeProjectPerTarget && TargetConfigurationType != TargetType.Game) { ProjectConfigurationName += "_" + TargetConfigurationType.ToString(); } if (ProjectConfigAndTargetCombinations.Any(Combination => Combination.ProjectPlatformName == ProjectPlatformName && Combination.ProjectConfigurationName == ProjectConfigurationName)) { throw new BuildException("'{0}' '{1} is already in the platform/config list. This means a platform generator is not marking that the config needs to be distinct.", ProjectPlatformName, ProjectConfigurationName); } ProjectConfigAndTargetCombinations.Add(new ProjectConfigAndTargetCombination(Platform, Configuration, ProjectPlatformName, ProjectConfigurationName, ProjectTarget, Arch)); }; UnrealArchitectures? Architectures = VCProjectFileGenerator.GetPlatformArchitecturesToGenerate(BuildPlatform, ProjectTarget); if (Architectures == null) { AddProjectAndTargetCombination(null); } else { if (BuildPlatform.ArchitectureConfig.Mode == UnrealArchitectureMode.SingleTargetLinkSeparately) { if (BuildPlatform.Platform == UnrealTargetPlatform.Android) { Architectures = BuildPlatform.ArchitectureConfig.AllSupportedArchitectures; } } foreach (UnrealArch Arch in Architectures.Architectures) { AddProjectAndTargetCombination(Arch); } } } } } } } // Create a list of platforms for the "invalid" configuration. We always require at least one of these. if (ProjectConfigAndTargetCombinations.Count == 0) { InvalidConfigPlatformNames = new HashSet { DefaultPlatformName }; } else { InvalidConfigPlatformNames = new HashSet(ProjectConfigAndTargetCombinations.Select(x => x.ProjectPlatformName)); } } } /// /// If found writes a debug project file to disk /// /// True on success public override List> WriteDebugProjectFiles(List InPlatforms, List InConfigurations, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger) { //string ProjectName = ProjectFilePath.GetFileNameWithoutExtension(); List ProjectPlatforms = new List(); List> ProjectFiles = new List>(); BuildProjectConfigAndTargetCombinations(InPlatforms, InConfigurations, PlatformProjectGenerators, Logger); foreach (ProjectConfigAndTargetCombination Combination in ProjectConfigAndTargetCombinations!) { if (Combination.Platform != null && !ProjectPlatforms.Contains(Combination.Platform.Value)) { ProjectPlatforms.Add(Combination.Platform.Value); } } //write out any additional project files if (!IsStubProject && ProjectTargets.Any(x => x.TargetRules != null && x.TargetRules.Type != TargetType.Program)) { foreach (UnrealTargetPlatform Platform in ProjectPlatforms) { PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(Platform, true); if (ProjGenerator != null) { //write out additional prop file ProjGenerator.WriteAdditionalPropFile(); //write out additional project user files ProjGenerator.WriteAdditionalProjUserFile(this); //write out additional project files Tuple? DebugProjectInfo = ProjGenerator.WriteAdditionalProjFile(this); if (DebugProjectInfo != null) { ProjectFiles.Add(DebugProjectInfo); } } } } return ProjectFiles; } private string[]? FilteredIncludeList = null; private string[]? FilteredPathsList = null; bool PathIsFilteredOut(DirectoryReference InPath, ref string[]? FilteredList) { // Turn the filter string into an array, remove whitespace, and normalize any path statements the first time // we are asked to check a path. if (FilteredList == null) { IEnumerable CleanPaths = Settings.ExcludedFilePaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) .Select(P => P.Trim()) .Select(P => P.Replace('/', Path.DirectorySeparatorChar)); FilteredList = CleanPaths.ToArray(); } // The user might have specified Foo, \Foo, Foo\, or \Foo\ as excludes. // Directory paths don't contain a trailing slash so add one to the string // we'll compare to our list so we can Contains and EndsWith to catch: // \Foo\Dir // \Foo string PathWithSeparator = InPath.FullName + Path.DirectorySeparatorChar; if (FilteredList.Length > 0) { foreach (string Entry in FilteredList) { if (PathWithSeparator.Contains(Entry, StringComparison.OrdinalIgnoreCase) || PathWithSeparator.EndsWith(Entry, StringComparison.OrdinalIgnoreCase)) { return true; } } } return false; } bool IncludePathIsFilteredOut(DirectoryReference IncludePath) { return PathIsFilteredOut(IncludePath, ref FilteredIncludeList); } bool FilePathIsFilteredOut(DirectoryReference InPath) { return PathIsFilteredOut(InPath, ref FilteredPathsList); } bool TryGetBuildEnvironment(DirectoryReference BaseDir, [NotNullWhen(true)] out BuildEnvironment? OutBuildEnvironment) { for (DirectoryReference? CurrentDir = BaseDir; CurrentDir != null; CurrentDir = CurrentDir.ParentDirectory) { BuildEnvironment? BuildEnvironment; if (BaseDirToBuildEnvironment.TryGetValue(CurrentDir, out BuildEnvironment)) { OutBuildEnvironment = BuildEnvironment; return true; } } OutBuildEnvironment = null; return false; } /// /// Append a list of include paths to a property list /// /// String builder for the property value /// Collection of include paths /// Set of paths to ignore void AppendIncludePaths(StringBuilder Builder, IncludePathsCollection Collection, HashSet IgnorePaths) { foreach (DirectoryReference IncludePath in Collection.AbsolutePaths) { if (!IgnorePaths.Contains(IncludePath) && !IncludePathIsFilteredOut(IncludePath)) { Builder.Append(NormalizeProjectPath(IncludePath.FullName)); Builder.Append(';'); } } } /// Implements Project interface public override bool WriteProjectFile(List InPlatforms, List InConfigurations, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger) { string ProjectName = ProjectFilePath.GetFileNameWithoutExtension(); bool bSuccess = true; // Setup VC project file content StringBuilder VCProjectFileContent = new StringBuilder(); StringBuilder VCFiltersFileContent = new StringBuilder(); StringBuilder VCUserFileContent = new StringBuilder(); VisualStudioUserFileSettings VCUserFileSettings = new VisualStudioUserFileSettings(); // Visual Studio doesn't require a *.vcxproj.filters file to even exist alongside the project unless // it actually has something of substance in it. We'll avoid saving it out unless we need to. bool FiltersFileIsNeeded = false; // Project file header VCProjectFileContent.AppendLine(""); VCProjectFileContent.AppendLine("", VCProjectFileGenerator.GetProjectFileToolVersionString(ProjectFileFormat)); bool bGenerateUserFileContent = PlatformProjectGenerators.PlatformRequiresVSUserFileGeneration(InPlatforms, InConfigurations); if (bGenerateUserFileContent) { VCUserFileContent.AppendLine(""); VCUserFileContent.AppendLine("", VCProjectFileGenerator.GetProjectFileToolVersionString(ProjectFileFormat)); } BuildProjectConfigAndTargetCombinations(InPlatforms, InConfigurations, PlatformProjectGenerators, Logger); VCProjectFileContent.AppendLine(" "); // Make a list of the platforms and configs as project-format names List ProjectPlatforms = new List(); List> ProjectPlatformNameAndPlatforms = new List>(); // ProjectPlatformName, Platform List> ProjectConfigurationNameAndConfigurations = new List>(); // ProjectConfigurationName, Configuration List ValidProjectConfigAndTargetCombinations = new List(); Dictionary> ProjectPlatformNameToPlatform = new Dictionary>(); foreach (ProjectConfigAndTargetCombination Combination in ProjectConfigAndTargetCombinations!) { if (Combination.Platform == null) { continue; } if (Combination.ProjectTarget != null && !IsValidProjectPlatformAndConfiguration(Combination.ProjectTarget, (UnrealTargetPlatform)Combination.Platform, Combination.Configuration, Logger)) { continue; } if (!ProjectPlatforms.Contains(Combination.Platform.Value)) { ProjectPlatforms.Add(Combination.Platform.Value); } if (!ProjectPlatformNameAndPlatforms.Any(ProjectPlatformNameAndPlatformTuple => ProjectPlatformNameAndPlatformTuple.Item1 == Combination.ProjectPlatformName)) { ProjectPlatformNameAndPlatforms.Add(Tuple.Create(Combination.ProjectPlatformName, Combination.Platform.Value)); } if (!ProjectConfigurationNameAndConfigurations.Any(ProjectConfigurationNameAndConfigurationTuple => ProjectConfigurationNameAndConfigurationTuple.Item1 == Combination.ProjectConfigurationName)) { ProjectConfigurationNameAndConfigurations.Add(Tuple.Create(Combination.ProjectConfigurationName, Combination.Configuration)); } ValidProjectConfigAndTargetCombinations.Add(Combination); if (ProjectPlatformNameToPlatform.TryGetValue(Combination.ProjectPlatformName, out HashSet? PlatformList)) { PlatformList.Add(Combination.Platform.Value); } else { ProjectPlatformNameToPlatform.Add(Combination.ProjectPlatformName, new HashSet() { Combination.Platform.Value }); } } // Add the "invalid" configuration for each platform. We use this when the solution configuration does not match any project configuration. foreach (string InvalidConfigPlatformName in InvalidConfigPlatformNames!) { VCProjectFileContent.AppendLine(" ", InvalidConfigPlatformName); VCProjectFileContent.AppendLine(" Invalid"); VCProjectFileContent.AppendLine(" {0}", InvalidConfigPlatformName); VCProjectFileContent.AppendLine(" "); } // Output ALL the project's config-platform permutations (project files MUST do this) foreach (Tuple ConfigurationTuple in ProjectConfigurationNameAndConfigurations) { string ProjectConfigurationName = ConfigurationTuple.Item1; foreach (Tuple PlatformTuple in ProjectPlatformNameAndPlatforms) { string ProjectPlatformName = PlatformTuple.Item1; VCProjectFileContent.AppendLine(" ", ProjectConfigurationName, ProjectPlatformName); VCProjectFileContent.AppendLine(" {0}", ProjectConfigurationName); VCProjectFileContent.AppendLine(" {0}", ProjectPlatformName); VCProjectFileContent.AppendLine(" "); } } VCProjectFileContent.AppendLine(" "); VCFiltersFileContent.AppendLine(""); VCFiltersFileContent.AppendLine("", VCProjectFileGenerator.GetProjectFileToolVersionString(ProjectFileFormat)); // Platform specific PropertyGroups, etc. if (!IsStubProject) { foreach (UnrealTargetPlatform Platform in ProjectPlatforms) { PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(Platform, true); VSSettings VSSettings = new(Platform, UnrealTargetConfiguration.Development, ProjectFileFormat, null); if (ProjGenerator != null && ProjGenerator.HasVisualStudioSupport(VSSettings)) { ProjGenerator.GetAdditionalVisualStudioPropertyGroups(VSSettings, VCProjectFileContent); } } } ProjectConfigAndTargetCombination? FoundCombo = ProjectConfigAndTargetCombinations.FirstOrDefault(combo => combo != null && combo.ProjectTarget != null && combo.ProjectTarget.TargetRules != null); TargetRules? DefaultRules = FoundCombo?.ProjectTarget?.TargetRules; bool IsTestTarget = (DefaultRules != null && DefaultRules.IsTestTarget); // Project globals (project GUID, project type, SCC bindings, etc) { VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" {0}", ProjectGUID.ToString("B").ToUpperInvariant()); VCProjectFileContent.AppendLine(" {0}", ProjectName); if (IsTestTarget) { VCProjectFileContent.AppendLine(" true"); } if (ProjectFileGenerator.bVisualStudioLinux) { VCProjectFileContent.AppendLine(" Linux"); VCProjectFileContent.AppendLine(" Linux"); VCProjectFileContent.AppendLine(" Generic"); VCProjectFileContent.AppendLine(" 1.0"); VCProjectFileContent.AppendLine(" {D51BCBC9-82E9-4017-911E-C93873C4EA2B}"); } VCProjectFileContent.AppendLine(" "); } // look for additional import lines for all platforms for non stub projects if (!IsStubProject) { foreach (UnrealTargetPlatform Platform in ProjectPlatforms) { VSSettings VSSettings = new(Platform, UnrealTargetConfiguration.Development, ProjectFileFormat, null); PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(Platform, true); if (ProjGenerator != null && ProjGenerator.HasVisualStudioSupport(VSSettings)) { ProjGenerator.GetVisualStudioGlobalProperties(VSSettings, VCProjectFileContent); } } } // Write each project configuration PreDefaultProps section HashSet CommonPlatformsWritten = new HashSet(); foreach (ProjectConfigAndTargetCombination Combination in ValidProjectConfigAndTargetCombinations) { if (Combination.Platform != null) { UnrealTargetPlatform TargetPlatform = Combination.Platform.Value; PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(TargetPlatform, true); if (ProjGenerator != null) { bool ProjectPlatformRequiresConfigurationName = false; if (ProjectPlatformNameToPlatform.TryGetValue(Combination.ProjectPlatformName, out HashSet? PlatformList)) { ProjectPlatformRequiresConfigurationName = PlatformList.Count > 1; } // Properties that are common for the unique platform // Note that if the platform name is not unique then the common properties will be added below if (!ProjectPlatformRequiresConfigurationName && !CommonPlatformsWritten.Contains(Combination.Platform.Value)) { StringBuilder CommonPlatformToolsetString = new StringBuilder(); ProjGenerator.GetVisualStudioPreDefaultString(TargetPlatform, CommonPlatformToolsetString); if (CommonPlatformToolsetString.Length > 0) { string ConditionString = "Condition=\"'$(Platform)'=='" + Combination.ProjectPlatformName + "'\""; VCProjectFileContent.AppendLine(" "); VCProjectFileContent.Append(CommonPlatformToolsetString); VCProjectFileContent.AppendLine(" "); } CommonPlatformsWritten.Add(TargetPlatform); } // Properties that require the configuration and platform name StringBuilder PlatformToolsetString = new StringBuilder(); if (ProjectPlatformRequiresConfigurationName) { ProjGenerator.GetVisualStudioPreDefaultString(TargetPlatform, PlatformToolsetString); } ProjGenerator.GetVisualStudioPreDefaultString(TargetPlatform, Combination.Configuration, PlatformToolsetString); if (PlatformToolsetString.Length > 0) { string ProjectConfigurationAndPlatformName = Combination.ProjectConfigurationName + "|" + Combination.ProjectPlatformName; string ConditionString = "Condition=\"'$(Configuration)|$(Platform)'=='" + ProjectConfigurationAndPlatformName + "'\""; VCProjectFileContent.AppendLine(" "); VCProjectFileContent.Append(PlatformToolsetString); VCProjectFileContent.AppendLine(" "); } } } } // Write the per platform/config configuration info foreach (Tuple ConfigurationTuple in ProjectConfigurationNameAndConfigurations) { string ProjectConfigurationName = ConfigurationTuple.Item1; UnrealTargetConfiguration TargetConfiguration = ConfigurationTuple.Item2; foreach (Tuple PlatformTuple in ProjectPlatformNameAndPlatforms) { string ProjectPlatformName = PlatformTuple.Item1; UnrealTargetPlatform TargetPlatform = PlatformTuple.Item2; PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(TargetPlatform, true); if (ProjGenerator == null) { continue; } PlatformProjectGenerator.VSSettings VSSettings = new(TargetPlatform, TargetConfiguration, ProjectFileFormat, null); StringBuilder PlatformToolsetString = new StringBuilder(); ProjGenerator.GetVisualStudioPlatformToolsetString(VSSettings, PlatformToolsetString); string PlatformConfigurationType = ProjGenerator.GetVisualStudioPlatformConfigurationType(VSSettings); // if we are using the defaults set earlier then skip writing this if (PlatformConfigurationType == PlatformProjectGenerator.DefaultPlatformConfigurationType && PlatformToolsetString.Length == 0) { continue; } string ProjectConfigurationAndPlatformName = ProjectConfigurationName + "|" + ProjectPlatformName; string ConditionString = "Condition=\"'$(Configuration)|$(Platform)'=='" + ProjectConfigurationAndPlatformName + "'\""; VCProjectFileContent.AppendLine(" ", ConditionString); VCProjectFileContent.AppendLine(" {0}", PlatformConfigurationType); if (PlatformToolsetString.Length == 0) { VCProjectFileGenerator.AppendPlatformToolsetProperty(VCProjectFileContent, ProjectFileFormat); } else { VCProjectFileContent.Append(PlatformToolsetString); } VCProjectFileContent.AppendLine(" "); } } VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); // Merge as many include paths as possible into the shared list HashSet SharedIncludeSearchPathsSet = new HashSet(); // Build up the new include search path string StringBuilder SharedIncludeSearchPaths = new StringBuilder(); { // Find out how many source files there are in each directory ConcurrentDictionary SourceDirToCount = new(); Parallel.ForEach(SourceFiles.Where(sf => sf.Reference.HasExtension(".cpp")), SourceFile => { SourceDirToCount.AddOrUpdate(SourceFile.Reference.Directory, _ => 1, (k, v) => v + 1); }); // Figure out the most common include paths ConcurrentDictionary IncludePathToCount = new(); Parallel.ForEach(SourceDirToCount, Pair => { if (TryGetBuildEnvironment(Pair.Key, out BuildEnvironment? OutBuildEnvironment)) { foreach (DirectoryReference IncludePath in OutBuildEnvironment.UserIncludePaths.AbsolutePaths) { IncludePathToCount.AddOrUpdate(IncludePath, _ => Pair.Value, (k, v) => v + Pair.Value); } foreach (DirectoryReference IncludePath in OutBuildEnvironment.SystemIncludePaths.AbsolutePaths) { IncludePathToCount.AddOrUpdate(IncludePath, _ => Pair.Value, (k, v) => v + Pair.Value); } return; } }); // Append the most common include paths to the search list. if (Settings.MaxSharedIncludePaths > 0) { foreach (DirectoryReference IncludePath in IncludePathToCount.OrderByDescending(x => x.Value).ThenBy(x => x.Key).Select(x => x.Key)) { string RelativePath = NormalizeProjectPath(IncludePath); if (SharedIncludeSearchPaths.Length + RelativePath.Length >= Settings.MaxSharedIncludePaths) { break; } if (!IncludePathIsFilteredOut(IncludePath)) { SharedIncludeSearchPathsSet.Add(IncludePath); SharedIncludeSearchPaths.AppendFormat("{0};", RelativePath); } } } SharedIncludeSearchPaths.AppendFormat("$(DefaultSystemIncludePaths);"); } // Gather source folder and file info List LocalAliasedFiles = new List(AliasedFiles); ConcurrentDictionary DirectoryToPchFile = new(); ConcurrentDictionary DirectoryToForceIncludePaths = new(); ConcurrentDictionary DirectoryToIncludeSearchPaths = new(); { foreach (SourceFile CurFile in SourceFiles) { // We want all source file and directory paths in the project files to be relative to the project file's // location on the disk. Convert the path to be relative to the project file directory string ProjectRelativeSourceFile = CurFile.Reference.MakeRelativeTo(ProjectFilePath.Directory); // By default, files will appear relative to the project file in the solution. This is kind of the normal Visual // Studio way to do things, but because our generated project files are emitted to intermediate folders, if we always // did this it would yield really ugly paths int he solution explorer string FilterRelativeSourceDirectory; if (CurFile.BaseFolder == null) { FilterRelativeSourceDirectory = ProjectRelativeSourceFile; } else { FilterRelativeSourceDirectory = CurFile.Reference.MakeRelativeTo(CurFile.BaseFolder); } // Manually remove the filename for the filter. We run through this code path a lot, so just do it manually. int LastSeparatorIdx = FilterRelativeSourceDirectory.LastIndexOf(Path.DirectorySeparatorChar); if (LastSeparatorIdx == -1) { FilterRelativeSourceDirectory = ""; } else { FilterRelativeSourceDirectory = FilterRelativeSourceDirectory.Substring(0, LastSeparatorIdx); } LocalAliasedFiles.Add(new AliasedFile(CurFile.Reference, ProjectRelativeSourceFile, FilterRelativeSourceDirectory)); } Parallel.ForEach(LocalAliasedFiles, LocalAliasedFile => { // get the filetype as represented to Visual Studio VCFileType FileType = GetVCFileType(LocalAliasedFile.FileSystemPath); DirectoryReference FileSystemPathDir = new DirectoryReference(LocalAliasedFile.FileSystemPath); // if the filetype is an include and its path is filtered out, skip it entirely (should we do this for any type of // file? Possibly, but not today due to potential fallout) if (CanVCFileTypeBeIncluded(FileType) && IncludePathIsFilteredOut(FileSystemPathDir)) { return; } // Allow filtering of any type of file if (FilePathIsFilteredOut(FileSystemPathDir)) { return; } if (!CanVCFileTypeBeCompiled(FileType)) { return; } DirectoryReference Directory = LocalAliasedFile.Location.Directory; // Find the PCH file DirectoryToPchFile.GetOrAdd(Directory, _ => { string? PchHeaderFile = null; for (DirectoryReference? ParentDir = Directory; ParentDir != null; ParentDir = ParentDir.ParentDirectory) { if (ModuleDirToPchHeaderFile.TryGetValue(ParentDir, out PchHeaderFile)) { break; } } return PchHeaderFile; }); // Find the force-included headers DirectoryToForceIncludePaths.GetOrAdd(Directory, _ => { string? ForceIncludePaths = null; for (DirectoryReference? ParentDir = Directory; ParentDir != null; ParentDir = ParentDir.ParentDirectory) { if (ModuleDirToForceIncludePaths.TryGetValue(ParentDir, out ForceIncludePaths)) { break; } } // filter here. It's a little more graceful to do it where this info is built but easier to follow if we filter // things our right before they're written. if (!String.IsNullOrEmpty(ForceIncludePaths)) { IEnumerable PathList = ForceIncludePaths.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); ForceIncludePaths = String.Join(";", PathList.Where(P => !IncludePathIsFilteredOut(new DirectoryReference(P)))); } ForceIncludePaths ??= String.Empty; return ForceIncludePaths; }); if (TryGetBuildEnvironment(Directory, out BuildEnvironment? BuildEnvironment)) { DirectoryToIncludeSearchPaths.GetOrAdd(Directory, _ => { StringBuilder Builder = new StringBuilder(); AppendIncludePaths(Builder, BuildEnvironment.UserIncludePaths, SharedIncludeSearchPathsSet); AppendIncludePaths(Builder, BuildEnvironment.SystemIncludePaths, SharedIncludeSearchPathsSet); return Builder.ToString(); }); } }); } // Check to see if all the source settings are the same string CommonForcedIncludes = String.Empty; string CommonAdditionalOptions = String.Empty; { if (DirectoryToForceIncludePaths.Any()) { string ForceIncludePathToCheck = DirectoryToForceIncludePaths.Values.First(); if (DirectoryToForceIncludePaths.Values.All(x => x == ForceIncludePathToCheck)) { CommonForcedIncludes = ForceIncludePathToCheck; DirectoryToForceIncludePaths.Clear(); } } if (DirectoryToPchFile.Any()) { string? PchFileToCheck = DirectoryToPchFile.Values.FirstOrDefault(); if (DirectoryToPchFile.Values.All(x => x == PchFileToCheck)) { if (!String.IsNullOrEmpty(PchFileToCheck)) { CommonAdditionalOptions = $"/Yu\"{PchFileToCheck}\""; } DirectoryToPchFile.Clear(); } } } StringBuilder VCPreprocessorDefinitions = new StringBuilder(); foreach (string CurDef in IntelliSensePreprocessorDefinitions) { if (VCPreprocessorDefinitions.Length > 0) { VCPreprocessorDefinitions.Append(';'); } VCPreprocessorDefinitions.Append(CurDef); } Func GetAdditionalOptionsString = (TargetRules? TargetRules) => { return String.Format("{0} {1}{2}{3}", GetCppStandardCompileArgument(GetIntelliSenseCppVersion()), GetEnableCoroutinesArgument(), DefaultRules != null ? (" " + GetConformanceCompileArguments(DefaultRules)) : String.Empty, CommonAdditionalOptions.Length > 0 ? (" " + CommonAdditionalOptions) : String.Empty); }; string DefaultAdditionalOptions = GetAdditionalOptionsString(DefaultRules); // Write common IntelliSense info { // @todo projectfiles: Currently we are storing defines/include paths for ALL configurations rather than using ConditionString and storing // this data uniquely for each target configuration. IntelliSense may behave better if we did that, but it will result in a LOT more // data being stored into the project file, and might make the IDE perform worse when switching configurations! VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" $(NMakePreprocessorDefinitions){0}", (VCPreprocessorDefinitions.Length > 0 ? (";" + VCPreprocessorDefinitions) : String.Empty)); // NOTE: Setting the IncludePath property rather than NMakeIncludeSearchPath results in significantly less // memory usage, because NMakeIncludeSearchPath metadata is duplicated to each output item. Functionality should be identical for // intellisense results. VCProjectFileContent.AppendLine(" $(IncludePath){0}", (SharedIncludeSearchPaths.Length > 0 ? (";" + SharedIncludeSearchPaths) : "")); VCProjectFileContent.AppendLine(" $(NMakeForcedIncludes){0}", (CommonForcedIncludes.Length > 0 ? (";" + CommonForcedIncludes) : "")); VCProjectFileContent.AppendLine(" $(NMakeAssemblySearchPath)"); VCProjectFileContent.AppendLine(" {0}", DefaultAdditionalOptions); VCProjectFileContent.AppendLine(" "); } // Write platform properties HashSet WrittenPlatforms = new(); foreach (ProjectConfigAndTargetCombination Combination in ProjectConfigAndTargetCombinations) { if (WrittenPlatforms.Add(Combination.Platform)) { StringBuilder PlatformProperties = new(); string PlatformAdditionalOptions = GetAdditionalOptionsString(Combination.ProjectTarget?.TargetRules); if (PlatformAdditionalOptions != DefaultAdditionalOptions) { PlatformProperties.AppendLine(" {0}", PlatformAdditionalOptions); } if (PlatformProperties.Length != 0) { VCProjectFileContent.AppendLine(" "); VCProjectFileContent.Append(PlatformAdditionalOptions); VCProjectFileContent.AppendLine(" "); } } } // Write each configuration { StringBuilder[] TmpVCProjectFileContent = new StringBuilder[ProjectConfigAndTargetCombinations.Count]; StringBuilder[] TmpVCUserFileContent = new StringBuilder[ProjectConfigAndTargetCombinations.Count]; Parallel.For(0, ProjectConfigAndTargetCombinations.Count, Index => { TmpVCProjectFileContent[Index] = new StringBuilder(); TmpVCUserFileContent[Index] = new StringBuilder(); WriteConfiguration(ProjectName, ProjectConfigAndTargetCombinations[Index], TmpVCProjectFileContent[Index], PlatformProjectGenerators, bGenerateUserFileContent ? TmpVCUserFileContent[Index] : null, bGenerateUserFileContent ? VCUserFileSettings : null); }); for (int Index = 0; Index < ProjectConfigAndTargetCombinations.Count; Index++) { if (TmpVCProjectFileContent[Index].Length > 0) { VCProjectFileContent.Append(TmpVCProjectFileContent[Index]); } if (TmpVCUserFileContent[Index].Length > 0) { VCUserFileContent.Append(TmpVCUserFileContent[Index]); } } } { // Collapse common values { StringBuilder CommonProjectFileContent = new StringBuilder(); Action, string, string, string> AddProperties = (IDictionary DirectoryToStringDict, string CommonPropertyPrefix, string PropertyNamePrefix, string PropertyValuePrefix) => { Dictionary UpdatedValues = new(); List> KVPList = DirectoryToStringDict.ToList(); KVPList.SortBy(kvp => kvp.Key); // Map each directory to a property { int PropertyIndex = 0; foreach (KeyValuePair DirectoryKVP in KVPList) { string? Value = DirectoryKVP.Value; if (!String.IsNullOrEmpty(Value)) { if (!UpdatedValues.ContainsKey(Value)) { string PropertyName = PropertyNamePrefix; if (PropertyIndex > 0) { PropertyName += "_" + PropertyIndex; } PropertyIndex++; UpdatedValues.Add(Value, PropertyName); } DirectoryToStringDict[DirectoryKVP.Key] = UpdatedValues[Value]; } } } // Find the common property values Dictionary ValueToCommonPropertyDict = new(); { Dictionary ValueAndCount = new(); foreach (string PropertyValue in UpdatedValues.Keys) { if (!String.IsNullOrEmpty(PropertyValue)) { IEnumerable PathList = PropertyValue.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); foreach (string Path in PathList) { if (ValueAndCount.TryGetValue(Path, out int PathCount)) { ValueAndCount[Path] = ++PathCount; } else { ValueAndCount[Path] = 1; } } } } List CommonProperties = ValueAndCount.Where(kvp => kvp.Value > 1).Select(kvp => kvp.Key).ToList(); CommonProperties.Sort(); // Write out the common property values int CommonPropertyValueIndex = 0; foreach (string? CommonProperty in CommonProperties) { string PropertyName = CommonPropertyPrefix; if (CommonPropertyValueIndex > 0) { PropertyName += "_" + CommonPropertyValueIndex; } CommonPropertyValueIndex++; ValueToCommonPropertyDict.Add(CommonProperty, $"$({PropertyName})"); CommonProjectFileContent.AppendLine($" <{PropertyName}>{CommonProperty}"); } } // Write out the updated properties foreach (KeyValuePair kvp in UpdatedValues) { string[] PathArray = kvp.Key.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); for (int Index = 0; Index < PathArray.Length; Index++) { if (ValueToCommonPropertyDict.TryGetValue(PathArray[Index], out string? PropertyValue) && !String.IsNullOrEmpty(PropertyValue)) { PathArray[Index] = PropertyValue; } } string NewValue = String.Join(";", PathArray); CommonProjectFileContent.AppendLine($" <{kvp.Value}>{PropertyValuePrefix}{NewValue}"); } }; //AdditionalIncludeDirectories AddProperties(DirectoryToIncludeSearchPaths, "ProjectAdditionalIncludeDirectories", "ClCompile_AdditionalIncludeDirectories", "$(NMakeIncludeSearchPath);"); //ForcedIncludeFiles AddProperties(DirectoryToForceIncludePaths, "ProjectForcedIncludeFiles", "ClCompile_ForcedIncludeFiles", String.Empty); // AdditionalOptions { int ValueIndex = 0; Dictionary UpdatedValues = new(); List> KVPList = DirectoryToPchFile.ToList(); KVPList.SortBy(kvp => kvp.Key); foreach (KeyValuePair DirectoryKVP in KVPList) { string? Value = DirectoryKVP.Value; if (!String.IsNullOrEmpty(Value)) { if (!UpdatedValues.ContainsKey(Value)) { string PropertyName = "ClCompile_AdditionalOptions"; if (ValueIndex > 0) { PropertyName += "_" + ValueIndex; } ValueIndex++; UpdatedValues.Add(Value, PropertyName); CommonProjectFileContent.AppendLine($" <{PropertyName}>$(AdditionalOptions) /Yu\"{Value}\""); } DirectoryToPchFile[DirectoryKVP.Key] = UpdatedValues[Value]; } } } if (CommonProjectFileContent.Length > 0) { VCProjectFileContent.AppendLine(" "); VCProjectFileContent.Append(CommonProjectFileContent); VCProjectFileContent.AppendLine(" "); } } VCFiltersFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); // Add all file directories to the filters file as solution filters HashSet FilterDirectories = new HashSet(); foreach (AliasedFile AliasedFile in LocalAliasedFiles) { // No need to add the root directory relative to the project (it would just be an empty string!) if (!String.IsNullOrWhiteSpace(AliasedFile.ProjectPath)) { FiltersFileIsNeeded = EnsureFilterPathExists(AliasedFile.ProjectPath, VCFiltersFileContent, FilterDirectories); } // get the filetype as represented to Visual Studio VCFileType FileType = GetVCFileType(AliasedFile.FileSystemPath); DirectoryReference FileSystemPathDir = new DirectoryReference(AliasedFile.FileSystemPath); // if the filetype is an include and its path is filtered out, skip it entirely (should we do this for any type of // file? Possibly, but not today due to potential fallout) if (CanVCFileTypeBeIncluded(FileType) && IncludePathIsFilteredOut(FileSystemPathDir)) { continue; } // Allow filtering of any type of file if (FilePathIsFilteredOut(FileSystemPathDir)) { continue; } string VCFileType = GetVCFileTypeString(FileType); if (!CanVCFileTypeBeCompiled(FileType)) { VCProjectFileContent.AppendLine(" <{0} Include=\"{1}\"/>", VCFileType, EscapeFileName(AliasedFile.FileSystemPath)); } else { DirectoryReference Directory = AliasedFile.Location.Directory; // Find the include search paths if (TryGetBuildEnvironment(Directory, out BuildEnvironment? BuildEnvironment)) { StringBuilder ClCompileInfo = new(); if (DirectoryToIncludeSearchPaths.TryGetValue(Directory, out string? DirectoryToIncludeSearchPathValue) && !String.IsNullOrEmpty(DirectoryToIncludeSearchPathValue)) { ClCompileInfo.AppendLine($" $({DirectoryToIncludeSearchPathValue})"); } if (DirectoryToForceIncludePaths.TryGetValue(Directory, out string? DirectoryToForceIncludePathValue) && !String.IsNullOrEmpty(DirectoryToForceIncludePathValue)) { ClCompileInfo.AppendLine($" $({DirectoryToForceIncludePathValue})"); } if (DirectoryToPchFile.Any()) { string? PchHeaderFile = DirectoryToPchFile[Directory]; if (PchHeaderFile != null && ProjectFileFormat >= VCProjectFileFormat.VisualStudio2022) { ClCompileInfo.AppendLine($" $({DirectoryToPchFile[Directory]})"); } } if (ClCompileInfo.Length == 0) { VCProjectFileContent.AppendLine(" <{0} Include=\"{1}\"/>", VCFileType, EscapeFileName(AliasedFile.FileSystemPath)); } else { VCProjectFileContent.AppendLine(" <{0} Include=\"{1}\">", VCFileType, EscapeFileName(AliasedFile.FileSystemPath)); VCProjectFileContent.Append(ClCompileInfo.ToString()); VCProjectFileContent.AppendLine(" ", VCFileType); } } else { VCProjectFileContent.AppendLine(" <{0} Include=\"{1}\"/>", VCFileType, EscapeFileName(AliasedFile.FileSystemPath)); } } if (!String.IsNullOrWhiteSpace(AliasedFile.ProjectPath)) { VCFiltersFileContent.AppendLine(" <{0} Include=\"{1}\">", VCFileType, EscapeFileName(AliasedFile.FileSystemPath)); VCFiltersFileContent.AppendLine(" {0}", Utils.CleanDirectorySeparators(EscapeFileName(AliasedFile.ProjectPath))); VCFiltersFileContent.AppendLine(" ", VCFileType); FiltersFileIsNeeded = true; } else { // No need to specify the root directory relative to the project (it would just be an empty string!) VCFiltersFileContent.AppendLine(" <{0} Include=\"{1}\" />", VCFileType, EscapeFileName(AliasedFile.FileSystemPath)); } } VCProjectFileContent.AppendLine(" "); VCFiltersFileContent.AppendLine(" "); } // For Installed engine builds, include engine source in the source search paths if it exists. We never build it locally, so the debugger can't find it. if (Unreal.IsEngineInstalled() && !IsStubProject) { VCProjectFileContent.AppendLine(" "); VCProjectFileContent.Append(" "); foreach (string DirectoryName in Directory.EnumerateDirectories(Unreal.EngineSourceDirectory.FullName, "*", SearchOption.AllDirectories)) { if (Directory.EnumerateFiles(DirectoryName, "*.cpp").Any()) { VCProjectFileContent.Append(DirectoryName); VCProjectFileContent.Append(';'); } } VCProjectFileContent.AppendLine(""); VCProjectFileContent.AppendLine(" "); } string OutputManifestString = ""; if (!IsStubProject) { foreach (UnrealTargetPlatform Platform in ProjectPlatforms) { VSSettings VSSettings = new(Platform, UnrealTargetConfiguration.Development, ProjectFileFormat, null); PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(Platform, true); if (ProjGenerator != null && ProjGenerator.HasVisualStudioSupport(VSSettings)) { // @todo projectfiles: Serious hacks here because we are trying to emit one-time platform-specific sections that need information // about a target type, but the project file may contain many types of targets! Some of this logic will need to move into // the per-target configuration writing code. TargetType HackTargetType = TargetType.Game; FileReference? HackTargetFilePath = null; foreach (ProjectConfigAndTargetCombination Combination in ProjectConfigAndTargetCombinations) { if (Combination.Platform == Platform && Combination.ProjectTarget!.TargetRules != null && Combination.ProjectTarget.TargetRules.Type == HackTargetType) { HackTargetFilePath = Combination.ProjectTarget.TargetFilePath;// ProjectConfigAndTargetCombinations[0].ProjectTarget.TargetFilePath; break; } } if (HackTargetFilePath != null) { OutputManifestString += ProjGenerator.GetVisualStudioOutputManifestSection(VSSettings, HackTargetType, HackTargetFilePath, ProjectFilePath); } } } } VCProjectFileContent.Append(OutputManifestString); // output manifest must come before the Cpp.targets file. VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); // Make sure CleanDependsOn is defined empty so the CppClean task isn't run when cleaning targets (use makefile instead) VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" $(CleanDependsOn); "); VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); FileReference[] VSTestRunSettingsFiles = ProjectTargets.Select(Target => Target.TargetRules?.VSTestRunSettingsFile) .Where(File => File != null).Select(File => File!).Distinct().ToArray(); if (VSTestRunSettingsFiles.Length == 1) { string RunSettingsRelativePath = VSTestRunSettingsFiles[0].MakeRelativeTo(ProjectFilePath.Directory); VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine($" $(ProjectDir){RunSettingsRelativePath}"); VCProjectFileContent.AppendLine(" "); } else if (VSTestRunSettingsFiles.Length > 1) { string Files = String.Join(", ", VSTestRunSettingsFiles.Select(File => File!.FullName)); Logger.LogWarning("Inconsistent VSTest run settings files for project '{ProjectFilePath}': {Files}", ProjectFilePath, Files); } if (!IsStubProject) { foreach (UnrealTargetPlatform Platform in ProjectPlatforms) { PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(Platform, true); VSSettings VSSettings = new(Platform, UnrealTargetConfiguration.Development, ProjectFileFormat, null); if (ProjGenerator != null && ProjGenerator.HasVisualStudioSupport(VSSettings)) { ProjGenerator.GetVisualStudioTargetOverrides(VSSettings, VCProjectFileContent); } } } VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); WriteTargets(ProjectPlatforms, PlatformProjectGenerators, VCProjectFileContent, Logger); VCProjectFileContent.AppendLine(""); VCFiltersFileContent.AppendLine(""); if (bGenerateUserFileContent) { VCUserFileContent.AppendLine(""); } // Save the project file if (bSuccess) { bSuccess = ProjectFileGenerator.WriteFileIfChanged(ProjectFilePath.FullName, VCProjectFileContent.ToString(), Logger); } // Save the filters file if (bSuccess) { // Create a path to the project file's filters file string VCFiltersFilePath = ProjectFilePath.FullName + ".filters"; if (FiltersFileIsNeeded) { bSuccess = ProjectFileGenerator.WriteFileIfChanged(VCFiltersFilePath, VCFiltersFileContent.ToString(), Logger); } else { Logger.LogDebug("Deleting Visual C++ filters file which is no longer needed: {File}", VCFiltersFilePath); // Delete the filters file, if one exists. We no longer need it try { File.Delete(VCFiltersFilePath); } catch (Exception) { Logger.LogInformation("Error deleting filters file (file may not be writable): {File}", VCFiltersFilePath); } } } // Save the user file, if required if (VCUserFileContent.Length > 0) { // Create a path to the project file's user file string VCUserFilePath = ProjectFilePath.FullName + ".user"; // Never overwrite the existing user path as it will cause them to lose their settings if (ProjectFileGenerator.bForceUpdateAllFiles || File.Exists(VCUserFilePath) == false) { bSuccess = ProjectFileGenerator.WriteFileIfChanged(VCUserFilePath, VCUserFileContent.ToString(), Logger); } else { bSuccess = PatchVCUserFile(VCUserFilePath, VCUserFileContent.ToString(), VCUserFileSettings, Logger); } } return bSuccess; } private bool PatchVCUserFile(string FileName, string NewFileContents, VisualStudioUserFileSettings UserFileSettings, ILogger Logger) { // Before we start, see if any relevant property is present in the new contents. if (UserFileSettings.PropertiesToPatch.Any(NewFileContents.Contains) == false) { return true; } XDocument CurrentContent; try { CurrentContent = XDocument.Load(FileName); } catch (Exception ex) { string Message = String.Format("Error while trying to parse XML file {0}.", FileName); Logger.LogError("{Message}", Message); throw new BuildException(ex, Message); } XDocument NewContent; try { NewContent = XDocument.Parse(NewFileContents); } catch (Exception ex) { string Message = String.Format("Error while trying to parse XML new data for file {0} ('{1}').", FileName, NewFileContents); Logger.LogError("{Message}", Message); throw new BuildException(ex, Message); } if (CurrentContent.Root == null || NewContent.Root == null) { return true; } XNamespace NS = NewContent.Root.Name.Namespace; // Skip patching if namespaces don't match (should never be the case?) if (NS != CurrentContent.Root.Name.Namespace) { return false; } // Create dictionaries with key == Condition of each and value == XElement itself for both current and new document. Dictionary CurrentPropertyGroups = CurrentContent .Descendants(NS + "PropertyGroup") .Select(Element => (Attribute: Element.Attribute("Condition"), Element)) .Where(Pair => Pair.Attribute != null) .ToDictionary(Pair => Pair.Attribute!.Value, Pair => Pair.Element); Dictionary NewPropertyGroups = NewContent .Descendants(NS + "PropertyGroup") .Select(Element => (Attribute: Element.Attribute("Condition"), Element)) .Where(Pair => Pair.Attribute != null) .ToDictionary(Pair => Pair.Attribute!.Value, Pair => Pair.Element); bool bNeedsSaving = false; // Go over every in new document. foreach ((string Attribute, XElement NewPropertyGroup) in NewPropertyGroups) { // Check if new document contains any properties that we need to patch in the current document. if (NewPropertyGroup.Elements().Any(Element => UserFileSettings.PropertiesToPatch.Contains(Element.Name.LocalName)) == false) { continue; } // Check if with same "Condition" attribute already exist in the current document. // If yes, update required properties in existing but preserve any other property in current document order. if (CurrentPropertyGroups.TryGetValue(Attribute, out XElement? CurrentPropertyGroup)) { // Preserve values from current document for relevant properties by patching corresponding properties in new document. IEnumerable ElementsToPreserveValuesFrom = CurrentPropertyGroup .Elements() .Where(Element => UserFileSettings.PropertiesToPatchOrderButPreserveValue.Contains(Element.Name.LocalName)); foreach (XElement CurrentElement in ElementsToPreserveValuesFrom) { XElement? NewElement = NewPropertyGroup.Element(CurrentElement.Name); if (NewElement != null) { NewElement.Value = CurrentElement.Value; } } XElement[] CurrentPropertyGroupElementsForPatch = CurrentPropertyGroup .Elements() .Where(Element => UserFileSettings.PropertiesToPatch.Contains(Element.Name.LocalName)) .ToArray(); XElement[] NewPropertyGroupElementsForPatch = NewPropertyGroup .Elements() .Where(Element => UserFileSettings.PropertiesToPatch.Contains(Element.Name.LocalName)) .ToArray(); // Check if all properties are already has the correct value and order, and skip patching if so. if (CurrentPropertyGroupElementsForPatch.Length == NewPropertyGroupElementsForPatch.Length && !CurrentPropertyGroupElementsForPatch.Where((CurrentProperty, i) => CurrentProperty.Name != NewPropertyGroupElementsForPatch[i].Name || CurrentProperty.Value != NewPropertyGroupElementsForPatch[i].Value).Any()) { continue; } // Remove all existing properties that we need to update from existing document, this simplifies logic of adding them, // because we need to update the values and ensure the order of properties is as declared in the new document, // because the order of properties is important for MSBuild when one property uses a value of another property. CurrentPropertyGroupElementsForPatch.Remove(); // Add new properties to existing in the order as they are defined in new document. CurrentPropertyGroup.Add(NewPropertyGroupElementsForPatch); bNeedsSaving = true; } else // Otherwise add new as-is to the end of existing document. { CurrentContent.Root.Add(NewPropertyGroup); bNeedsSaving = true; } } if (bNeedsSaving) { try { CurrentContent.Save(FileName); Logger.LogDebug("Patching {Path}.", Path.GetFileName(FileName)); } catch (Exception ex) { string Message = String.Format("Error while trying to write file {0}. The file is probably read-only.", FileName); Logger.LogError("{Message}", Message); throw new BuildException(ex, Message); } } else { Logger.LogDebug("{Path} doesn't require patching.", Path.GetFileName(FileName)); } return true; } private class ProjectConfigurationForGenerator : ProjectBuildConfiguration { public override string ConfigurationName => Combination.ProjectConfigurationName; public override string BuildCommand => $"{EscapePath(NormalizeProjectPath(CommandBuilder.BuildScript))} {CommandBuilder.GetBuildArguments()}"; private ProjectConfigAndTargetCombination Combination; private BuildCommandBuilder CommandBuilder; public ProjectConfigurationForGenerator(ProjectConfigAndTargetCombination InCombination, BuildCommandBuilder InCommandBuilder) { Combination = InCombination; CommandBuilder = InCommandBuilder; } } /// /// Write additional Target elements if needed by per-platform generators. /// /// /// /// /// private void WriteTargets(List ProjectPlatforms, PlatformProjectGeneratorCollection PlatformProjectGenerators, StringBuilder VCProjectFileContent, ILogger Logger) { foreach (UnrealTargetPlatform Platform in ProjectPlatforms) { PlatformProjectGenerator? ProjGenerator = PlatformProjectGenerators.GetPlatformProjectGenerator(Platform, true); if (ProjGenerator != null && ProjGenerator.HasVisualStudioTargets(Platform)) { IEnumerable PlatformCombinations = ProjectConfigAndTargetCombinations!.Where(Combination => Combination.Platform == Platform); if (PlatformCombinations.Any()) { List PlatformConfigurations = PlatformCombinations.Select(Combination => { string UProjectPath = ""; if (IsForeignProject) { UProjectPath = String.Format("\"{0}\"", InsertPathVariables(Combination.ProjectTarget!.UnrealProjectFilePath!)); } BuildCommandBuilder Builder = CreateArgumentsBuilder(Combination, UProjectPath, ProjGenerator); return new ProjectConfigurationForGenerator(Combination, Builder) as ProjectBuildConfiguration; } ).ToList(); ProjGenerator.GetVisualStudioTargetsString(Platform, PlatformConfigurations, VCProjectFileContent); } } } } private static bool EnsureFilterPathExists(string FilterRelativeSourceDirectory, StringBuilder VCFiltersFileContent, HashSet FilterDirectories) { // We only want each directory to appear once in the filters file string PathRemaining = Utils.CleanDirectorySeparators(FilterRelativeSourceDirectory); bool FiltersFileIsNeeded = false; if (!FilterDirectories.Contains(PathRemaining)) { // Make sure all subdirectories leading up to this directory each have their own filter, too! List AllDirectoriesInPath = new List(); string PathSoFar = ""; for (; ; ) { if (PathRemaining.Length > 0) { int SlashIndex = PathRemaining.IndexOf(Path.DirectorySeparatorChar); string SplitDirectory; if (SlashIndex != -1) { SplitDirectory = PathRemaining.Substring(0, SlashIndex); PathRemaining = PathRemaining.Substring(SplitDirectory.Length + 1); } else { SplitDirectory = PathRemaining; PathRemaining = ""; } if (!String.IsNullOrEmpty(PathSoFar)) { PathSoFar += Path.DirectorySeparatorChar; } PathSoFar += SplitDirectory; AllDirectoriesInPath.Add(PathSoFar); } else { break; } } foreach (string LeadingDirectory in AllDirectoriesInPath) { if (!FilterDirectories.Contains(LeadingDirectory)) { FilterDirectories.Add(LeadingDirectory); // Generate a unique stable GUID for this folder by hashing the LeadingDirectory string // NOTE: When saving generated project files, we ignore differences in GUIDs if every other part of the file // matches identically with the pre-existing file string FilterGUID = VCProjectFileGenerator.MakeMd5Guid(LeadingDirectory).ToString("B").ToUpperInvariant(); VCFiltersFileContent.AppendLine(" ", EscapeFileName(LeadingDirectory)); VCFiltersFileContent.AppendLine(" {0}", FilterGUID); VCFiltersFileContent.AppendLine(" "); FiltersFileIsNeeded = true; } } } return FiltersFileIsNeeded; } private enum VCFileType { None, CCode, Header, Inline, Resource, Manifest, }; /// /// Returns the VCFileType string based on the VCFileType enum. /// /// /// Name of the element in MSBuild project file for this file type private string GetVCFileTypeString(VCFileType FileType) { switch (FileType) { default: case VCFileType.None: return "None"; case VCFileType.CCode: return "ClCompile"; case VCFileType.Header: return Settings.bHeadersAsClCompile ? "ClCompile" : "ClInclude"; case VCFileType.Inline: return "ClInclude"; case VCFileType.Resource: return "ResourceCompile"; case VCFileType.Manifest: return "Manifest"; } } private bool CanVCFileTypeBeIncluded(VCFileType FileType) { return FileType == VCFileType.Header || FileType == VCFileType.Inline; } private bool CanVCFileTypeBeCompiled(VCFileType FileType) { return FileType == VCFileType.CCode || (FileType == VCFileType.Header && Settings.bHeadersAsClCompile); } /// /// Returns the VCFileType enum value based on the file path. /// /// The path of the file to return type for. /// VCFileType enum value for this file. private VCFileType GetVCFileType(string Path) { // What type of file is this? if (Path.EndsWith(".h", StringComparison.InvariantCultureIgnoreCase)) { return VCFileType.Header; } if (Path.EndsWith(".inl", StringComparison.InvariantCultureIgnoreCase)) { return VCFileType.Inline; } if (Path.EndsWith(".cpp", StringComparison.InvariantCultureIgnoreCase)) { return VCFileType.CCode; } if (Path.EndsWith(".rc", StringComparison.InvariantCultureIgnoreCase)) { return VCFileType.Resource; } if (Path.EndsWith(".manifest", StringComparison.InvariantCultureIgnoreCase)) { return VCFileType.Manifest; } return VCFileType.None; } // Helper class to generate NMake build commands and arguments public class BuildCommandBuilder { public bool bEditorDependsOnShaderCompileWorker = true; public bool bBuildLiveCodingConsole; public bool bAddFastPDBToProjects; public bool bIsForeignProject; public bool bUsePrecompiled; public bool bIsFromMSBuild; public PlatformProjectGenerator? ProjectGenerator; public FileReference BuildScript { get; } public FileReference RebuildScript { get; } public FileReference CleanScript { get; } private readonly string? BuildToolOverride; private readonly string UProjectPath; private readonly VSSettings VSSettings; private readonly ProjectTarget ProjectTarget; public BuildCommandBuilder(VSSettings InVSSettings, ProjectTarget InProjectTarget, string InUProjectPath, string? InBuildToolOverride = null) { VSSettings = InVSSettings; ProjectTarget = InProjectTarget; UProjectPath = InUProjectPath; BuildToolOverride = InBuildToolOverride; DirectoryReference BatchFilesDirectory = DirectoryReference.Combine(Unreal.EngineDirectory, "Build", "BatchFiles"); BuildScript = FileReference.Combine(BatchFilesDirectory, "Build.bat"); RebuildScript = FileReference.Combine(BatchFilesDirectory, "Rebuild.bat"); CleanScript = FileReference.Combine(BatchFilesDirectory, "Clean.bat"); } public string GetBuildArguments() { TargetRules TargetRulesObject = ProjectTarget.TargetRules!; string TargetName = ProjectTarget.TargetFilePath.GetFileNameWithoutAnyExtensions(); StringBuilder BuildArguments = new StringBuilder(); BuildArguments.AppendFormat("{0} {1} {2}", TargetName, VSSettings.Platform.ToString(), VSSettings.Configuration.ToString()); if (UProjectPath.Length > 0) { BuildArguments.AppendFormat(" -Project={0}", UProjectPath); } List ExtraTargets = new List(); if (!bUsePrecompiled) { if (TargetRulesObject.Type == TargetType.Editor && bEditorDependsOnShaderCompileWorker && !Unreal.IsEngineInstalled()) { string ProjParam = UProjectPath.Length > 0 ? $" -Project=\\\"{UProjectPath.Trim('"')}\\\"" : ""; ExtraTargets.Add($"ShaderCompileWorker Win64 Development{ProjParam}"); } if (TargetRulesObject.bWithLiveCoding && 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"); if (bIsFromMSBuild) { BuildArguments.Append(" -FromMsBuild"); } if (bAddFastPDBToProjects) { // Pass Fast PDB option to make use of Visual Studio's /DEBUG:FASTLINK option BuildArguments.Append(" -FastPDB"); } if (BuildToolOverride != null) { BuildArguments.AppendFormat(" {0}", BuildToolOverride); } if (ProjectGenerator != null) { BuildArguments.Append(ProjectGenerator.GetExtraBuildArguments(VSSettings)); } if (VSSettings.Architecture != null) { BuildArguments.AppendFormat(" -architecture={0}", VSSettings.Architecture); } return BuildArguments.ToString(); } } private BuildCommandBuilder CreateArgumentsBuilder(ProjectConfigAndTargetCombination Combination, string UProjectPath, PlatformProjectGenerator? ProjGenerator) { BuildCommandBuilder Builder = new BuildCommandBuilder(new VSSettings(Combination.Platform!.Value, Combination.Configuration, ProjectFileFormat, Combination.Architecture), Combination.ProjectTarget!, UProjectPath, BuildToolOverride) { ProjectGenerator = ProjGenerator, bEditorDependsOnShaderCompileWorker = Settings.bEditorDependsOnShaderCompileWorker, bBuildLiveCodingConsole = Settings.bBuildLiveCodingConsole, bAddFastPDBToProjects = Settings.bAddFastPDBToProjects, bIsForeignProject = IsForeignProject, bUsePrecompiled = bUsePrecompiled, // Always include a flag to format log messages for MSBuild bIsFromMSBuild = true }; return Builder; } // Anonymous function that writes project configuration data private void WriteConfiguration(string ProjectName, ProjectConfigAndTargetCombination Combination, StringBuilder VCProjectFileContent, PlatformProjectGeneratorCollection PlatformProjectGenerators, StringBuilder? VCUserFileContent, VisualStudioUserFileSettings? VCUserFileSettings) { UnrealTargetConfiguration Configuration = Combination.Configuration; PlatformProjectGenerator? ProjGenerator = Combination.Platform != null ? PlatformProjectGenerators.GetPlatformProjectGenerator(Combination.Platform.Value, true) : null; FileReference? UProjectPathNullable = Combination.ProjectTarget?.UnrealProjectFilePath; string UProjectPath = ""; if (UProjectPathNullable != null) { UProjectPath = String.Format("\"{0}\"", InsertPathVariables(UProjectPathNullable)); } string ConditionString = "Condition=\"'$(Configuration)|$(Platform)'=='" + Combination.ProjectConfigurationAndPlatformName + "'\""; { // Add custom import info if (ProjGenerator != null) { StringBuilder CustomImportGroupInfo = new StringBuilder(); ProjGenerator.GetVisualStudioImportGroupProperties(new(Combination.Platform!.Value, Configuration, ProjectFileFormat, null), CustomImportGroupInfo); if (CustomImportGroupInfo.Length != 0) { VCProjectFileContent.AppendLine(" ", ConditionString); VCProjectFileContent.Append(CustomImportGroupInfo); VCProjectFileContent.AppendLine(" "); } } DirectoryReference ProjectDirectory = ProjectFilePath.Directory; FileReference? NMakePath = null; if (IsStubProject) { VCProjectFileContent.AppendLine(" ", ConditionString); VCProjectFileContent.AppendLine(" @rem Nothing to do."); VCProjectFileContent.AppendLine(" @rem Nothing to do."); VCProjectFileContent.AppendLine(" @rem Nothing to do."); if (ProjectFileGenerator.bVisualStudioLinux) { VCProjectFileContent.AppendLine(" $(NMakeBuildCommandLine)"); VCProjectFileContent.AppendLine(" $(NMakeReBuildCommandLine)"); VCProjectFileContent.AppendLine(" $(NMakeCleanCommandLine)"); VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); } VCProjectFileContent.AppendLine(" "); } else if (Unreal.IsEngineInstalled() && Combination.ProjectTarget != null && Combination.ProjectTarget.TargetRules != null && (Combination.Platform == null || !Combination.ProjectTarget.SupportedPlatforms.Contains(Combination.Platform.Value))) { string TargetName = Combination.ProjectTarget.TargetFilePath.GetFileNameWithoutAnyExtensions(); string ValidPlatforms = String.Join(", ", Combination.ProjectTarget.SupportedPlatforms.Select(x => x.ToString())); VCProjectFileContent.AppendLine(" ", ConditionString); VCProjectFileContent.AppendLine(" @echo {0} is not a supported platform for {1}. Valid platforms are {2}.", Combination.Platform!, TargetName, ValidPlatforms); VCProjectFileContent.AppendLine(" @echo {0} is not a supported platform for {1}. Valid platforms are {2}.", Combination.Platform!, TargetName, ValidPlatforms); VCProjectFileContent.AppendLine(" @echo {0} is not a supported platform for {1}. Valid platforms are {2}.", Combination.Platform!, TargetName, ValidPlatforms); VCProjectFileContent.AppendLine(" "); if (ProjectFileGenerator.bVisualStudioLinux) { VCProjectFileContent.AppendLine(" $(NMakeBuildCommandLine)"); VCProjectFileContent.AppendLine(" $(NMakeReBuildCommandLine)"); VCProjectFileContent.AppendLine(" $(NMakeCleanCommandLine)"); VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" "); } VCProjectFileContent.AppendLine(" "); } else { UnrealTargetPlatform Platform = Combination.Platform!.Value; TargetRules TargetRulesObject; try { if (Combination.Architecture != null) { TargetRulesObject = Combination.ProjectTarget!.CreateRulesDelegateWithArch(Platform, Configuration, (UnrealArch)Combination.Architecture); } else { TargetRulesObject = Combination.ProjectTarget!.CreateRulesDelegate(Platform, Configuration); } } catch (BuildException) { TargetRulesObject = Combination.ProjectTarget!.TargetRules!; } FileReference TargetFilePath = Combination.ProjectTarget.TargetFilePath; string TargetName = TargetFilePath.GetFileNameWithoutAnyExtensions(); string UBTPlatformName = Platform.ToString(); string UBTConfigurationName = Configuration.ToString(); VSSettings VSSettings = new(Platform, Configuration, ProjectFileFormat, null); if (Combination.Architecture != null) { VSSettings.Architecture = Combination.Architecture; } // Setup output path UEBuildPlatform BuildPlatform = UEBuildPlatform.GetBuildPlatform(Platform); // Figure out if this is a monolithic build bool bShouldCompileMonolithic = BuildPlatform.ShouldCompileMonolithicBinary(Platform) | (TargetRulesObject.LinkType == TargetLinkType.Monolithic); // Get the .uproject directory DirectoryReference? UProjectDirectory = DirectoryReference.FromFile(Combination.ProjectTarget.UnrealProjectFilePath); // Get the output directory DirectoryReference RootOutputDirectory; if (UProjectDirectory != null && (bShouldCompileMonolithic || TargetRulesObject.BuildEnvironment == TargetBuildEnvironment.Unique) && TargetRulesObject.File!.IsUnderDirectory(UProjectDirectory)) { RootOutputDirectory = UEBuildTarget.GetOutputDirectoryForExecutable(UProjectDirectory, TargetRulesObject.File!); } else { RootOutputDirectory = UEBuildTarget.GetOutputDirectoryForExecutable(Unreal.EngineDirectory, TargetRulesObject.File!); } // Get the output directory DirectoryReference OutputDirectory = DirectoryReference.Combine(RootOutputDirectory, "Binaries", UBTPlatformName); if (!String.IsNullOrEmpty(TargetRulesObject.ExeBinariesSubFolder)) { OutputDirectory = DirectoryReference.Combine(OutputDirectory, TargetRulesObject.ExeBinariesSubFolder); } // Get the executable name (minus any platform or config suffixes) string BaseExeName = TargetName; if (!bShouldCompileMonolithic && TargetRulesObject.Type != TargetType.Program && TargetRulesObject.BuildEnvironment != TargetBuildEnvironment.Unique) { BaseExeName = "Unreal" + TargetRulesObject.Type.ToString(); } // Make the output file path NMakePath = FileReference.Combine(OutputDirectory, BaseExeName); if (Configuration != TargetRulesObject.UndecoratedConfiguration) { NMakePath += TargetRulesObject.DecoratedSeparator + UBTPlatformName + TargetRulesObject.DecoratedSeparator + UBTConfigurationName; } if (UnrealArchitectureConfig.ForPlatform(Platform).RequiresArchitectureFilenames(TargetRulesObject.Architectures)) { NMakePath += TargetRulesObject.Architecture.ToString(); } else if (Combination.Architecture != null) // support the case where the project/platform combination explicitly sets an architecture(e.g. Win64) { UnrealArch Architecture = (UnrealArch)Combination.Architecture; if (UnrealArchitectureConfig.ForPlatform(Platform).RequiresArchitectureFilenames(new UnrealArchitectures(Architecture))) { NMakePath += Architecture.ToString(); } } NMakePath += BuildPlatform.GetBinaryExtension(UEBuildBinaryType.Executable); if (TargetRulesObject.OutputFile != null) { NMakePath = FileReference.Combine(RootOutputDirectory, TargetRulesObject.OutputFile); } VCProjectFileContent.AppendLine(" ", ConditionString); if (ProjGenerator != null) { StringBuilder PathsStringBuilder = new StringBuilder(); ProjGenerator.GetVisualStudioPathsEntries(VSSettings, TargetRulesObject.Type, TargetFilePath, ProjectFilePath, NMakePath, PathsStringBuilder); VCProjectFileContent.Append(PathsStringBuilder.ToString()); } // This is the standard UE based project NMake build line: // ..\..\Build\BatchFiles\Build.bat // ie ..\..\Build\BatchFiles\Build.bat BlankProgram Win64 Debug BuildCommandBuilder Builder = CreateArgumentsBuilder(Combination, UProjectPath, ProjGenerator); string BuildArguments = Builder.GetBuildArguments(); // NMake Build command line if (TargetRulesObject.IsTestTarget) { VCProjectFileContent.AppendLine(" $(BuildBatchScript) {0} -Mode=Test", BuildArguments); VCProjectFileContent.AppendLine(" $(BuildBatchScript) {0} -Mode=Test -RebuildTests", BuildArguments); VCProjectFileContent.AppendLine(" $(BuildBatchScript) {0} -Mode=Test -CleanTests", BuildArguments); } else { VCProjectFileContent.AppendLine(" $(BuildBatchScript) {0}", BuildArguments); VCProjectFileContent.AppendLine(" $(RebuildBatchScript) {0}", BuildArguments); VCProjectFileContent.AppendLine(" $(CleanBatchScript) {0}", BuildArguments); } if (TargetRulesObject.bBuildConsoleAppOnly) { VCProjectFileContent.AppendLine(" {0}", NormalizeProjectPath(UEBuildBinary.GetAdditionalConsoleAppPath(new FileReference(NMakePath.FullName)))); } else { VCProjectFileContent.AppendLine(" {0}", NormalizeProjectPath(NMakePath.FullName)); } if (ProjectFileGenerator.bVisualStudioLinux) { VCProjectFileContent.AppendLine(" $(NMakeBuildCommandLine)"); VCProjectFileContent.AppendLine(" $(NMakeReBuildCommandLine)"); VCProjectFileContent.AppendLine(" $(NMakeCleanCommandLine)"); if (TargetRulesObject.Platform.IsInGroup(UnrealPlatformGroup.Linux)) { VCProjectFileContent.AppendLine(" {0};{1};{2}", NormalizeProjectPath(NMakePath), NormalizeProjectPath(NMakePath.ChangeExtension(".debug")), NormalizeProjectPath(NMakePath.ChangeExtension(".sym"))); VCProjectFileContent.AppendLine(" chmod +x $(RemoteDeployDir)/{0}", NMakePath.GetFileName()); VCProjectFileContent.AppendLine(" $(RemoteDeployDir)/{0}", NMakePath.GetFileName()); } } if (TargetRulesObject.Type == TargetType.Game || TargetRulesObject.Type == TargetType.Client || TargetRulesObject.Type == TargetType.Server) { // Allow platforms to add any special properties they require PlatformProjectGenerators.GenerateGamePlatformSpecificProperties(Platform, Configuration, TargetRulesObject.Type, VCProjectFileContent, RootOutputDirectory, TargetFilePath); } VCProjectFileContent.AppendLine(" "); if (ProjGenerator != null) { VCProjectFileContent.Append(ProjGenerator.GetVisualStudioLayoutDirSection(VSSettings, ConditionString, Combination.ProjectTarget.TargetRules!.Type, Combination.ProjectTarget.TargetFilePath, ProjectFilePath, NMakePath)); } VCProjectFileContent.AppendLine(" ", ConditionString); VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" $(BuildBatchScript) {0} -WorkingDir=$(MSBuildProjectDirectory) -Files=$(SelectedFiles)", BuildArguments); VCProjectFileContent.AppendLine(" "); if (TargetRulesObject.bIsBuildingConsoleApplication) { // Let Visual Studio keep the console window open when debugging stops. Ignored by the build. VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" Console"); VCProjectFileContent.AppendLine(" "); } if (ProjectFileGenerator.bVisualStudioLinux && TargetRulesObject.Platform.IsInGroup(UnrealPlatformGroup.Linux)) { VCProjectFileContent.AppendLine(" "); VCProjectFileContent.AppendLine(" {0}:=$(RemoteDeployDir)/{1};{2}:=$(RemoteDeployDir)/{3};{4}:=$(RemoteDeployDir)/{5}", NormalizeProjectPath(NMakePath), NMakePath.GetFileName(), NormalizeProjectPath(NMakePath.ChangeExtension(".debug")), NMakePath.ChangeExtension(".debug").GetFileName(), NormalizeProjectPath(NMakePath.ChangeExtension(".sym")), NMakePath.ChangeExtension(".sym").GetFileName()); VCProjectFileContent.AppendLine(" "); } VCProjectFileContent.AppendLine(" "); } if (VCUserFileContent != null && VCUserFileSettings != null && Combination.ProjectTarget != null) { TargetRules TargetRulesObject = Combination.ProjectTarget.TargetRules!; if (ProjGenerator != null) { string? ForeignUProjectPath = (IsForeignProject && !String.IsNullOrEmpty(UProjectPath)) ? UProjectPath : null; VCUserFileContent.Append(ProjGenerator.GetVisualStudioUserFileStrings(VCUserFileSettings, new(Combination.Platform!.Value, Configuration, ProjectFileFormat, Combination.Architecture), ConditionString, TargetRulesObject, Combination.ProjectTarget.TargetFilePath, ProjectFilePath, NMakePath, ProjectName, ForeignUProjectPath)); } } } } } /// /// A Visual C# project. /// class VCSharpProjectFile : MSBuildProjectFile { /// /// This is the GUID that Visual Studio uses to identify a C# project file in the solution /// public override string ProjectTypeGUID => "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; /// /// Platforms that this project supports /// public HashSet Platforms = new HashSet(); /// /// Configurations that this project supports /// public HashSet Configurations = new HashSet(); /// /// Constructs a new project file object /// /// The path to the project file on disk /// Logger for output /// The base directory for files within this project - if not specified, InitFilePath.Directory will be used public VCSharpProjectFile(FileReference InitFilePath, ILogger Logger, DirectoryReference? BaseDir = null) : base(InitFilePath, BaseDir ?? InitFilePath.Directory) { try { XmlDocument Document = new XmlDocument(); Document.Load(InitFilePath.FullName); // Check the root element is the right type if (Document.DocumentElement?.Name != "Project") { throw new BuildException("Unexpected root element '{0}' in project file", Document.DocumentElement?.Name); } // Parse all the configurations and platforms // Parse the basic structure of the document, updating properties and recursing into other referenced projects as we go if (!IsDotNETCoreProject()) { foreach (XmlElement Element in Document.DocumentElement.ChildNodes.OfType()) { if (Element.Name == "PropertyGroup") { string Condition = Element.GetAttribute("Condition"); if (!String.IsNullOrEmpty(Condition)) { Match Match = Regex.Match(Condition, "^\\s*'\\$\\(Configuration\\)\\|\\$\\(Platform\\)'\\s*==\\s*'(.+)\\|(.+)'\\s*$"); if (Match.Success && Match.Groups.Count == 3) { Configurations.Add(Match.Groups[1].Value); Platforms.Add(Match.Groups[2].Value); } else { Logger.LogWarning("Unable to parse configuration/platform from condition '{InitFilePath}': {Condition}", InitFilePath, Condition); } } } } } else { foreach (string c in GetProjectProperty("Configurations").Split(';')) { Configurations.Add(c); } bool ConfigurationsFound = Configurations.Any(); if (!ConfigurationsFound) { foreach (XmlElement PropertyGroup in Document.DocumentElement.ChildNodes.OfType() .Where(element => element.Name == "PropertyGroup")) { XmlNodeList ConfigNodeList = PropertyGroup.GetElementsByTagName("Configurations"); // if this property group does not set configurations we do not care about it if (ConfigNodeList.Count == 0) { continue; } if (PropertyGroup.HasAttribute("Condition")) { string Condition = PropertyGroup.GetAttribute("Condition"); Logger.LogWarning("Unable to parse configuration from property group with condition '{InitFilePath}': {Condition}. UBT Requires you to set the configuration without conditionals.", InitFilePath, Condition); continue; } string[]? ParsedConfigurations = ConfigNodeList[0]?.FirstChild?.Value?.Split(';'); if (ParsedConfigurations != null) { foreach (string c in ParsedConfigurations) { Configurations.Add(c); } } // platforms change meaning quite a bit in .net core but typically you do not specify this and its derived from the build instead // for most intents it is just Any CPU from .net framework Platforms.Add("AnyCPU"); ConfigurationsFound = true; break; } } // dotnet does not require you to specify configurations or platforms, if you do not debug and release are the defaults if (!ConfigurationsFound) { Configurations.Add("Debug"); Configurations.Add("Release"); Platforms.Add("AnyCPU"); } } } catch (Exception Ex) { Logger.LogWarning("Unable to parse {Path}: {Ex}", InitFilePath, Ex.ToString()); } } /// /// Extract information from the csproj file based on the supplied configuration /// public CsProjectInfo? GetProjectInfo(UnrealTargetConfiguration InConfiguration) { if (CachedProjectInfo.ContainsKey(InConfiguration)) { return CachedProjectInfo[InConfiguration]; } CsProjectInfo? Info; Dictionary Properties = new Dictionary(); Properties.Add("Platform", "AnyCPU"); Properties.Add("Configuration", InConfiguration.ToString()); Properties.Add("EngineDirectory", Unreal.EngineDirectory.FullName); if (CsProjectInfo.TryRead(ProjectFilePath, Properties, out Info)) { CachedProjectInfo.Add(InConfiguration, Info); } return Info; } /// /// Determine if this project is a .NET Core project /// public bool IsDotNETCoreProject() { CsProjectInfo Info = GetProjectInfo(UnrealTargetConfiguration.Debug)!; return Info.IsDotNETCoreProject(); } /// /// Gets a property from the project /// public string GetProjectProperty(string property) { CsProjectInfo Info = GetProjectInfo(UnrealTargetConfiguration.Debug)!; Info.Properties.TryGetValue(property, out string? value); return value ?? String.Empty; } /// public override MSBuildProjectContext? GetMatchingProjectContext(TargetType SolutionTarget, UnrealTargetConfiguration SolutionConfiguration, UnrealTargetPlatform SolutionPlatform, PlatformProjectGeneratorCollection PlatformProjectGenerators, UnrealArch? Architecture, ILogger Logger) { // Find the matching platform name string ProjectPlatformName; if (Platforms.Contains("x64")) { ProjectPlatformName = "x64"; } else { ProjectPlatformName = "Any CPU"; } // Find the matching configuration string ProjectConfigurationName; if (Configurations.Contains(SolutionConfiguration.ToString())) { ProjectConfigurationName = SolutionConfiguration.ToString(); } else if (Configurations.Contains("Development")) { ProjectConfigurationName = "Development"; } else { ProjectConfigurationName = "Release"; } // Figure out whether to build it by default bool bBuildByDefault = ShouldBuildByDefaultForSolutionTargets; if (SolutionTarget == TargetType.Game || SolutionTarget == TargetType.Editor) { bBuildByDefault = true; } // Create the context return new MSBuildProjectContext(ProjectConfigurationName, ProjectPlatformName) { bBuildByDefault = bBuildByDefault }; } /// /// Basic csproj file support. Generates C# library project with one build config. /// /// Not used. /// Not Used. /// Set of platform project generators /// /// true if the opration was successful, false otherwise public override bool WriteProjectFile(List InPlatforms, List InConfigurations, PlatformProjectGeneratorCollection PlatformProjectGenerators, ILogger Logger) { throw new BuildException("Support for writing C# projects from UnrealBuildTool has been removed."); } /// Cache of parsed info about this project protected readonly Dictionary CachedProjectInfo = new Dictionary(); } }