// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using EpicGames.Core; namespace UnrealBuildTool { /// /// The version format for .uproject files. This rarely changes now; project descriptors should maintain backwards compatibility automatically. /// enum ProjectDescriptorVersion { /// /// Invalid /// Invalid = 0, /// /// Initial version /// Initial = 1, /// /// Adding SampleNameHash /// NameHash = 2, /// /// Unifying plugin/project files (since abandoned, but backwards compatibility maintained) /// ProjectPluginUnification = 3, /// /// This needs to be the last line, so we can calculate the value of Latest below /// LatestPlusOne, /// /// The latest plugin descriptor version /// Latest = LatestPlusOne - 1 } /// /// In-memory representation of a .uproject file /// public class ProjectDescriptor { /// /// Descriptor version number. /// public int FileVersion; /// /// The engine to open this project with. /// public string? EngineAssociation; /// /// Category to show under the project browser /// public string? Category; /// /// Description to show in the project browser /// public string? Description; /// /// List of all modules associated with this project /// public ModuleDescriptor[]? Modules; /// /// List of plugins for this project (may be enabled/disabled) /// public PluginReferenceDescriptor[]? Plugins; /// /// Array of additional root directories /// public List AdditionalRootDirectories = new List(); /// /// List of additional plugin directories to scan for available plugins /// public List AdditionalPluginDirectories = new List(); /// /// Array of platforms that this project is targeting /// public string[]? TargetPlatforms; /// /// A hash that is used to determine if the project was forked from a sample /// public uint EpicSampleNameHash; /// /// Steps to execute before creating rules assemblies in this project /// public CustomBuildSteps? InitSteps; /// /// Steps to execute before building targets in this project /// public CustomBuildSteps? PreBuildSteps; /// /// Steps to execute before building targets in this project /// public CustomBuildSteps? PostBuildSteps; /// /// Indicates if this project is an Enterprise project /// public bool IsEnterpriseProject; /// /// Indicates that enabled by default engine plugins should not be enabled unless explicitly enabled by the project or target files. /// public bool DisableEnginePluginsByDefault; /// /// Constructor. /// public ProjectDescriptor() { FileVersion = (int)ProjectDescriptorVersion.Latest; IsEnterpriseProject = false; DisableEnginePluginsByDefault = false; } /// /// Constructor /// /// Raw JSON object to parse /// Base directory for resolving relative paths /// public ProjectDescriptor(JsonObject RawObject, DirectoryReference BaseDir, FileReference JsonFilePath) { // Read the version if (!RawObject.TryGetIntegerField("FileVersion", out FileVersion)) { if (!RawObject.TryGetIntegerField("ProjectFileVersion", out FileVersion)) { throw new BuildException("Project does not contain a valid FileVersion entry"); } } // Check it's not newer than the latest version we can parse if (FileVersion > (int)PluginDescriptorVersion.Latest) { throw new BuildException("Project descriptor appears to be in a newer version ({0}) of the file format that we can load (max version: {1}).", FileVersion, (int)ProjectDescriptorVersion.Latest); } // Read simple fields RawObject.TryGetStringField("EngineAssociation", out EngineAssociation); RawObject.TryGetStringField("Category", out Category); RawObject.TryGetStringField("Description", out Description); RawObject.TryGetBoolField("Enterprise", out IsEnterpriseProject); RawObject.TryGetBoolField("DisableEnginePluginsByDefault", out DisableEnginePluginsByDefault); // Read the modules JsonObject[]? ModulesArray; if (RawObject.TryGetObjectArrayField("Modules", out ModulesArray)) { Modules = Array.ConvertAll(ModulesArray, x => ModuleDescriptor.FromJsonObject(x, JsonFilePath)); } // Read the plugins JsonObject[]? PluginsArray; if (RawObject.TryGetObjectArrayField("Plugins", out PluginsArray)) { Plugins = Array.ConvertAll(PluginsArray, x => PluginReferenceDescriptor.FromJsonObject(x)); } // Read the additional root directories string[]? RootDirectoryStrings; if (RawObject.TryGetStringArrayField("AdditionalRootDirectories", out RootDirectoryStrings)) { AdditionalRootDirectories.AddRange(RootDirectoryStrings.Select(x => DirectoryReference.Combine(BaseDir, x))); } // Read the additional plugin directories string[]? PluginDirectoryStrings; if (RawObject.TryGetStringArrayField("AdditionalPluginDirectories", out PluginDirectoryStrings)) { AdditionalPluginDirectories.AddRange(PluginDirectoryStrings.Select(x => DirectoryReference.Combine(BaseDir, x))); } // Read the target platforms RawObject.TryGetStringArrayField("TargetPlatforms", out TargetPlatforms); // Get the sample name hash RawObject.TryGetUnsignedIntegerField("EpicSampleNameHash", out EpicSampleNameHash); // Read the init, pre and post-build steps CustomBuildSteps.TryRead(RawObject, "InitSteps", out InitSteps); CustomBuildSteps.TryRead(RawObject, "PreBuildSteps", out PreBuildSteps); CustomBuildSteps.TryRead(RawObject, "PostBuildSteps", out PostBuildSteps); } /// /// Creates a plugin descriptor from a file on disk /// /// The filename to read /// New plugin descriptor public static ProjectDescriptor FromFile(FileReference FileName) { try { JsonObject RawObject = JsonObject.Read(FileName); ProjectDescriptor Descriptor = new ProjectDescriptor(RawObject, FileName.Directory, FileName); if (Descriptor.Modules != null) { foreach (ModuleDescriptor Module in Descriptor.Modules) { Module.Validate(FileName); } } return Descriptor; } catch (JsonException ex) { throw new JsonException($"{ex.Message} (in {FileName})", FileName.FullName, ex.LineNumber, ex.BytePositionInLine, ex); } } /// /// Creates a plugin descriptor based on a directory, searching for best uproject /// /// The directory to search for a uproject /// New plugin descriptor public static ProjectDescriptor FromDirectory(DirectoryReference DirectoryName) { FileReference ProjectFile = FileReference.Combine(DirectoryName, DirectoryName.GetDirectoryName() + ".uproject"); if (FileReference.Exists(ProjectFile)) { return ProjectDescriptor.FromFile(ProjectFile); } // find any uproject file, in case the name doesn't match IEnumerable FoundProjects = DirectoryReference.EnumerateFiles(DirectoryName, "*.uproject", SearchOption.TopDirectoryOnly); if (FoundProjects.Count() == 0) { throw new FileNotFoundException($"Unable to find a .uproject file in {DirectoryName}"); } if (FoundProjects.Count() > 1) { Log.TraceWarningOnce("Found multiple uproject files in {0}, choosing {1}", DirectoryName, FoundProjects.First().GetFileName()); } return ProjectDescriptor.FromFile(FoundProjects.First()); } /// /// If the descriptor has either additional plugin directories or additional root directories /// then it is considered to have additional paths. The additional paths will be relative /// to the provided directory. /// /// The directory set to add the paths too. /// The directory which is used to setup the additional paths public void AddAdditionalPaths(List RootDirectories, DirectoryReference ProjectDir) { RootDirectories.AddRange(AdditionalRootDirectories); RootDirectories.AddRange(AdditionalPluginDirectories); } /// /// Saves the descriptor to disk /// /// The filename to write to public void Save(FileReference FileName) { using (JsonWriter Writer = new JsonWriter(FileName)) { Writer.WriteObjectStart(); Write(Writer, FileName.Directory); Writer.WriteObjectEnd(); } } /// /// Writes the plugin descriptor to an existing Json writer /// /// The writer to receive plugin data /// Base directory to save paths relative to public void Write(JsonWriter Writer, DirectoryReference BaseDir) { Writer.WriteValue("FileVersion", (int)ProjectDescriptorVersion.Latest); Writer.WriteValue("EngineAssociation", EngineAssociation); Writer.WriteValue("Category", Category); Writer.WriteValue("Description", Description); if (DisableEnginePluginsByDefault) { Writer.WriteValue("DisableEnginePluginsByDefault", DisableEnginePluginsByDefault); } // Write the enterprise flag if (IsEnterpriseProject) { Writer.WriteValue("Enterprise", IsEnterpriseProject); } // Write the module list ModuleDescriptor.WriteArray(Writer, "Modules", Modules); // Write the plugin list PluginReferenceDescriptor.WriteArray(Writer, "Plugins", Plugins); // Write the custom module roots if (AdditionalRootDirectories.Count > 0) { Writer.WriteStringArrayField("AdditionalRootDirectories", AdditionalRootDirectories.Select(x => x.MakeRelativeTo(BaseDir).Replace(Path.DirectorySeparatorChar, '/'))); } // Write out the additional plugin directories to scan if (AdditionalPluginDirectories.Count > 0) { Writer.WriteStringArrayField("AdditionalPluginDirectories", AdditionalPluginDirectories.Select(x => x.MakeRelativeTo(BaseDir).Replace(Path.DirectorySeparatorChar, '/'))); } // Write the target platforms if (TargetPlatforms != null && TargetPlatforms.Length > 0) { Writer.WriteStringArrayField("TargetPlatforms", TargetPlatforms); } // If it's a signed sample, write the name hash if (EpicSampleNameHash != 0) { Writer.WriteValue("EpicSampleNameHash", (uint)EpicSampleNameHash); } // Write the custom build steps InitSteps?.Write(Writer, "InitSteps"); PreBuildSteps?.Write(Writer, "PreBuildSteps"); PostBuildSteps?.Write(Writer, "PostBuildSteps"); } } }