// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; using System.Xml.Serialization; using EpicGames.Core; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Type of a build product /// public enum BuildProductType { /// /// An executable file /// Executable, /// /// A dynamically loaded module. /// DynamicLibrary, /// /// A symbol file. Not required for the executable to run. /// SymbolFile, /// /// A map file. Not required for the executable to run. /// MapFile, /// /// A resource file which was generated by the build and is required for the executable to run. /// RequiredResource, /// /// A build resource which was generated by the build, but is not required for the executable to run. /// BuildResource, /// /// A package which can be deployed on device (eg. *.apk for Android, *.stub for iOS) /// Package } /// /// A file that was created as part of the build process /// [Serializable] public class BuildProduct { /// /// Path to the file. /// public FileReference Path; /// /// Type of the build product. /// public BuildProductType Type; /// /// Private constructor, for serialization. /// private BuildProduct() { Path = null!; } /// /// Constructor. /// /// Path to the build product /// Type of the build product public BuildProduct(FileReference InPath, BuildProductType InType) { Path = InPath; Type = InType; } /// /// Copy constructor. /// /// Build product to copy settings from public BuildProduct(BuildProduct Other) { Path = Other.Path; Type = Other.Type; } /// /// Convert this object to a string, for debugging. /// /// Path to this build product public override string ToString() { return Path.ToString(); } } /// /// How a file may be staged /// public enum StagedFileType { /// /// Only accessed through Unreal filesystem functions; may be included in a PAK file. /// UFS, /// /// Must be kept as part of the loose filesystem. /// NonUFS, /// /// Debug file which must be kept as part of the loose filesystem. /// DebugNonUFS, /// /// System file which must be kept as part of the loose filesystem. System files are not subject to being automatic remapping or renaming by the platform layer. /// SystemNonUFS, } /// /// Information about a file which is required by the target at runtime, and must be moved around with it. /// [Serializable] public class RuntimeDependency { /// /// The file that should be staged. Should use $(EngineDir) and $(ProjectDir) variables as a root, so that the target can be relocated to different machines. /// public FileReference Path; /// /// How to stage this file. /// public StagedFileType Type; /// /// Private constructor, for serialization. /// private RuntimeDependency() { Path = null!; } /// /// Constructor /// /// Path to the runtime dependency /// How to stage the given path public RuntimeDependency(FileReference InPath, StagedFileType InType = StagedFileType.NonUFS) { Path = InPath; Type = InType; } /// /// Copy constructor /// /// Runtime dependency to copy settings from public RuntimeDependency(RuntimeDependency InOther) { Path = InOther.Path; Type = InOther.Type; } /// /// Convert this object to a string for debugging /// /// String representation of the object public override string ToString() { return Path.ToString(); } } /// /// List of runtime dependencies, with convenience methods for adding new items /// [Serializable] public class RuntimeDependencyList : List { /// /// Default constructor /// public RuntimeDependencyList() { } /// /// Copy constructor /// /// Sequence of runtime dependencies to initialize with public RuntimeDependencyList(IEnumerable Other) : base(Other) { } /// /// Add a runtime dependency to the list /// /// Path to the runtime dependency. May include wildcards. /// How to stage this file public void Add(FileReference InPath, StagedFileType InType) { Add(new RuntimeDependency(InPath, InType)); } } /// /// Arbitrary property name/value which metadata from the build scripts can be passed on to downstream tasks /// [Serializable] public class ReceiptProperty { /// /// Property name /// [XmlAttribute] public string Name; /// /// Value of the property /// [XmlAttribute] public string Value; /// /// Construct a property with the given name and value /// /// Name of the property /// Value of the property public ReceiptProperty(string InName, string InValue) { Name = InName; Value = InValue; } } /// /// Stores a record of a built target, with all metadata that other tools may need to know about the build. /// [Serializable] public class TargetReceipt { /// /// Path to the project file for this target /// public FileReference? ProjectFile; /// /// The project directory /// public DirectoryReference? ProjectDir; /// /// The name of this target /// public string TargetName; /// /// Which platform the target is compiled for /// public UnrealTargetPlatform Platform; /// /// Which platform the target is compiled for /// public UnrealArchitectures Architectures; /// /// Which configuration this target is compiled in /// public UnrealTargetConfiguration Configuration; /// /// The type of the target /// public TargetType TargetType; /// /// Whether it's a low level tests target /// public bool IsTestTarget; /// /// Version information for this target. /// public BuildVersion Version; /// /// The exectuable to launch for this target /// public FileReference? Launch; /// /// The console subsystem/commmandlet exectuable to launch for this target /// public FileReference? LaunchCmd; /// /// The build products which are part of this target /// public List BuildProducts = new List(); /// /// All the runtime dependencies that this target relies on /// public RuntimeDependencyList RuntimeDependencies = new RuntimeDependencyList(); /// /// All plugins that were either enabled or disabled via the target rules. /// public Dictionary PluginNameToEnabledState = new Dictionary(); /// /// All plugins that were built via the target rules. /// public List BuildPlugins = new List(); /// /// Additional build properties passed through from the module rules /// public List AdditionalProperties = new List(); /// /// Constructor /// /// Path to the project file for this target /// The name of the target being compiled /// The type of target /// Platform for the target being compiled /// Configuration of the target being compiled /// Version information for the target /// Architecture information for the target /// Whether it's a target for low level tests public TargetReceipt(FileReference? InProjectFile, string InTargetName, TargetType InTargetType, UnrealTargetPlatform InPlatform, UnrealTargetConfiguration InConfiguration, BuildVersion InVersion, UnrealArchitectures InArchitectures, bool InIsTestTarget) { ProjectFile = InProjectFile; ProjectDir = DirectoryReference.FromFile(InProjectFile); TargetName = InTargetName; Platform = InPlatform; Configuration = InConfiguration; TargetType = InTargetType; Version = InVersion; Architectures = InArchitectures; IsTestTarget = InIsTestTarget; } /// /// Adds a build product to the receipt. Does not check whether it already exists. /// /// Path to the build product. /// Type of build product. /// The BuildProduct object that was created public BuildProduct AddBuildProduct(FileReference Path, BuildProductType Type) { BuildProduct NewBuildProduct = new BuildProduct(Path, Type); BuildProducts.Add(NewBuildProduct); return NewBuildProduct; } /// /// Merges another receipt to this one. /// /// Receipt which should be merged public void Merge(TargetReceipt Other) { foreach (BuildProduct OtherBuildProduct in Other.BuildProducts) { BuildProducts.Add(OtherBuildProduct); } foreach (RuntimeDependency OtherRuntimeDependency in Other.RuntimeDependencies) { if (!RuntimeDependencies.Any(x => x.Path == OtherRuntimeDependency.Path)) { RuntimeDependencies.Add(OtherRuntimeDependency); } } foreach (KeyValuePair Pair in Other.PluginNameToEnabledState) { if (!PluginNameToEnabledState.ContainsKey(Pair.Key)) { PluginNameToEnabledState.Add(Pair.Key, Pair.Value); } } foreach (string PluginName in Other.BuildPlugins) { if (!BuildPlugins.Contains(PluginName)) { BuildPlugins.Add(PluginName); } } } /// /// Inserts variables to make a file relative to $(EngineDir) or $(ProjectDir) /// /// The file to insert variables into. /// Value of the $(EngineDir) variable. /// Value of the $(ProjectDir) variable. /// Converted path for the file. static string InsertPathVariables(FileReference File, DirectoryReference EngineDir, DirectoryReference? ProjectDir) { if (File.IsUnderDirectory(EngineDir)) { return "$(EngineDir)/" + File.MakeRelativeTo(EngineDir).Replace(Path.DirectorySeparatorChar, '/'); } else if (ProjectDir != null && File.IsUnderDirectory(ProjectDir)) { return "$(ProjectDir)/" + File.MakeRelativeTo(ProjectDir).Replace(Path.DirectorySeparatorChar, '/'); } else { return File.FullName; } } /// /// Inserts variables to make a file relative to $(EngineDir) or $(ProjectDir) /// /// The path to insert variables into. /// Value of the $(EngineDir) variable. /// Value of the $(ProjectDir) variable. /// Converted path for the file. static FileReference ExpandPathVariables(string Path, DirectoryReference EngineDir, DirectoryReference? ProjectDir) { const string EnginePrefix = "$(EngineDir)"; if (Path.StartsWith(EnginePrefix, StringComparison.InvariantCultureIgnoreCase)) { return new FileReference(EngineDir.FullName + Path.Substring(EnginePrefix.Length)); } const string ProjectPrefix = "$(ProjectDir)"; if (ProjectDir != null && Path.StartsWith(ProjectPrefix, StringComparison.InvariantCultureIgnoreCase)) { return new FileReference(ProjectDir.FullName + Path.Substring(ProjectPrefix.Length)); } return new FileReference(Path); } /// /// Returns the standard path to the build receipt for a given target /// /// Base directory for the target being built; either the project directory or engine directory. /// The target being built /// The target platform /// The target configuration /// The architecture being built /// Path to the receipt for this target public static FileReference GetDefaultPath(DirectoryReference BaseDir, string TargetName, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, UnrealArchitectures? BuildArchitectures) { // Get the architecture suffix. Platforms have the option of overriding whether to include this string in filenames. string ArchitectureSuffix = ""; // @todo disallow null here? if (BuildArchitectures != null && UnrealArchitectureConfig.ForPlatform(Platform).RequiresArchitectureFilenames(BuildArchitectures)) { ArchitectureSuffix = BuildArchitectures.ToString(); } // Build the output filename if (String.IsNullOrEmpty(ArchitectureSuffix) && Configuration == UnrealTargetConfiguration.Development) { return FileReference.Combine(BaseDir, "Binaries", Platform.ToString(), String.Format("{0}.target", TargetName)); } else { return FileReference.Combine(BaseDir, "Binaries", Platform.ToString(), String.Format("{0}-{1}-{2}{3}.target", TargetName, Platform.ToString(), Configuration.ToString(), ArchitectureSuffix)); } } /// /// Determine whether a BuildProduct Type is NonUFS or DebugNonUFS /// /// The build product to check public static StagedFileType GetStageTypeFromBuildProductType(BuildProduct BuildProduct) { if (BuildProduct.Type == BuildProductType.SymbolFile || BuildProduct.Type == BuildProductType.MapFile) { return StagedFileType.DebugNonUFS; } return StagedFileType.NonUFS; } /// /// Checks the Additional properties for one with the given name that matches the given value /// /// Property name to search for /// Value to compare against (with StringComparison.InvariantCultureIgnoreCase) /// True if any property with PropertyName has a value matching Value public bool HasValueForAdditionalProperty(string PropertyName, string Value) { // get all properties with the given name? IEnumerable Results = AdditionalProperties.Where(x => x.Name == PropertyName); foreach (ReceiptProperty Property in Results) { // does the property value match? if (Property.Value.Equals(Value, StringComparison.InvariantCultureIgnoreCase)) { return true; } } return false; } /// /// Read a receipt from disk. /// /// Filename to read from public static TargetReceipt Read(FileReference Location) { return Read(Location, Unreal.EngineDirectory); } /// /// Read a receipt from disk. /// /// Filename to read from /// Engine directory for expanded variables public static TargetReceipt Read(FileReference Location, DirectoryReference EngineDir) { JsonObject RawObject = JsonObject.Read(Location); // Read the initial fields string TargetName = RawObject.GetStringField("TargetName"); TargetType TargetType = RawObject.GetEnumField("TargetType"); UnrealTargetPlatform Platform = UnrealTargetPlatform.Parse(RawObject.GetStringField("Platform")); UnrealTargetConfiguration Configuration = RawObject.GetEnumField("Configuration"); // Try to read the build version BuildVersion? Version; if (!BuildVersion.TryParse(RawObject.GetObjectField("Version"), out Version)) { throw new JsonException("Invalid 'Version' field", Location.FullName, null, null); } // Read the project path FileReference? ProjectFile; string? RelativeProjectFile; if (RawObject.TryGetStringField("Project", out RelativeProjectFile)) { ProjectFile = FileReference.Combine(Location.Directory, RelativeProjectFile); } else { ProjectFile = null; } // Read the launch executable string? Architecture; UnrealArchitectures Architectures; if (RawObject.TryGetStringField("Architecture", out Architecture)) { Architectures = UnrealArchitectures.FromString(Architecture, Platform)!; } else { // @todo this doesn't necessarily match how it was compiled - should this be an error case? Architectures = UnrealArchitectureConfig.ForPlatform(Platform).ActiveArchitectures(ProjectFile, TargetName); } bool IsTestTarget; RawObject.TryGetBoolField("IsTestTarget", out IsTestTarget); // Create the receipt TargetReceipt Receipt = new TargetReceipt(ProjectFile, TargetName, TargetType, Platform, Configuration, Version, Architectures, IsTestTarget); // Get the project directory DirectoryReference? ProjectDir = Receipt.ProjectDir; // Read the launch executable string? Launch; if (RawObject.TryGetStringField("Launch", out Launch)) { Receipt.Launch = ExpandPathVariables(Launch, EngineDir, ProjectDir); } string? LaunchCmd; if (RawObject.TryGetStringField("LaunchCmd", out LaunchCmd)) { Receipt.LaunchCmd = ExpandPathVariables(LaunchCmd, EngineDir, ProjectDir); } // Read the build products JsonObject[]? BuildProductObjects; if (RawObject.TryGetObjectArrayField("BuildProducts", out BuildProductObjects)) { foreach (JsonObject BuildProductObject in BuildProductObjects) { string? Path; BuildProductType Type; if (BuildProductObject.TryGetStringField("Path", out Path) && BuildProductObject.TryGetEnumField("Type", out Type)) { FileReference File = ExpandPathVariables(Path, EngineDir, ProjectDir); string? Module; BuildProductObject.TryGetStringField("Module", out Module); Receipt.AddBuildProduct(File, Type); } } } // Read the runtime dependencies JsonObject[]? RuntimeDependencyObjects; if (RawObject.TryGetObjectArrayField("RuntimeDependencies", out RuntimeDependencyObjects)) { foreach (JsonObject RuntimeDependencyObject in RuntimeDependencyObjects) { string? Path; if (RuntimeDependencyObject.TryGetStringField("Path", out Path)) { FileReference File = ExpandPathVariables(Path, EngineDir, ProjectDir); StagedFileType Type; if (!RuntimeDependencyObject.TryGetEnumField("Type", out Type)) { // Previous format included an optional IgnoreIfMissing flag, which was only used for debug files. We can explicitly reference them as DebugNonUFS files now. bool bIgnoreIfMissing; if (RuntimeDependencyObject.TryGetBoolField("IgnoreIfMissing", out bIgnoreIfMissing)) { bIgnoreIfMissing = false; } Type = bIgnoreIfMissing ? StagedFileType.DebugNonUFS : StagedFileType.NonUFS; } Receipt.RuntimeDependencies.Add(File, Type); } } } // Read the enabled/disabled plugins JsonObject[]? PluginObjects; if (RawObject.TryGetObjectArrayField("Plugins", out PluginObjects)) { foreach (JsonObject PluginObject in PluginObjects) { string? PluginName; if (PluginObject.TryGetStringField("Name", out PluginName)) { bool PluginEnabled; if (PluginObject.TryGetBoolField("Enabled", out PluginEnabled)) { Receipt.PluginNameToEnabledState.Add(PluginName, PluginEnabled); } } } } // Read the build plugins string[]? BuildPlugins; if (RawObject.TryGetStringArrayField("BuildPlugins", out BuildPlugins)) { Receipt.BuildPlugins.AddAll(BuildPlugins); } // Read the additional properties JsonObject[]? AdditionalPropertyObjects; if (RawObject.TryGetObjectArrayField("AdditionalProperties", out AdditionalPropertyObjects)) { foreach (JsonObject AdditionalPropertyObject in AdditionalPropertyObjects) { string? Name; if (AdditionalPropertyObject.TryGetStringField("Name", out Name)) { string? Value; if (AdditionalPropertyObject.TryGetStringField("Value", out Value)) { Receipt.AdditionalProperties.Add(new ReceiptProperty(Name, Value)); } } } } return Receipt; } /// /// Try to read a receipt from disk, failing gracefully if it can't be read. /// /// Filename to read from /// If successful, the receipt that was read /// True if successful public static bool TryRead(FileReference Location, [NotNullWhen(true)] out TargetReceipt? Receipt) { return TryRead(Location, Unreal.EngineDirectory, out Receipt); } /// /// Try to read a receipt from disk, failing gracefully if it can't be read. /// /// Filename to read from /// Engine directory for expanded paths /// If successful, the receipt that was read /// True if successful public static bool TryRead(FileReference Location, DirectoryReference EngineDir, [NotNullWhen(true)] out TargetReceipt? Receipt) { if (!FileReference.Exists(Location)) { Receipt = null; return false; } try { Receipt = Read(Location, EngineDir); return true; } catch (Exception) { Receipt = null; return false; } } /// /// Write the receipt to disk. /// /// Output filename public void Write(FileReference Location) { Write(Location, Unreal.EngineDirectory); } /// /// Write the receipt to disk. /// /// Output filename /// Engine directory for expanded paths public void Write(FileReference Location, DirectoryReference EngineDir) { using (JsonWriter Writer = new JsonWriter(Location.FullName)) { Writer.WriteObjectStart(); Writer.WriteValue("TargetName", TargetName); Writer.WriteValue("Platform", Platform.ToString()); Writer.WriteValue("Configuration", Configuration.ToString()); Writer.WriteValue("TargetType", TargetType.ToString()); Writer.WriteValue("IsTestTarget", IsTestTarget); Writer.WriteValue("Architecture", Architectures.ToString()); if (ProjectFile != null) { Writer.WriteValue("Project", ProjectFile.MakeRelativeTo(Location.Directory).Replace(Path.DirectorySeparatorChar, '/')); } if (Launch != null) { Writer.WriteValue("Launch", InsertPathVariables(Launch, EngineDir, ProjectDir)); } if (LaunchCmd != null) { Writer.WriteValue("LaunchCmd", InsertPathVariables(LaunchCmd, EngineDir, ProjectDir)); } Writer.WriteObjectStart("Version"); Version.WriteProperties(Writer); Writer.WriteObjectEnd(); Writer.WriteArrayStart("BuildProducts"); foreach (BuildProduct BuildProduct in BuildProducts.OrderBy(x => x.Path.FullName)) { Writer.WriteObjectStart(); Writer.WriteValue("Path", InsertPathVariables(BuildProduct.Path, EngineDir, ProjectDir)); Writer.WriteValue("Type", BuildProduct.Type.ToString()); Writer.WriteObjectEnd(); } Writer.WriteArrayEnd(); Writer.WriteArrayStart("RuntimeDependencies"); foreach (RuntimeDependency RuntimeDependency in RuntimeDependencies.OrderBy(x => x.Path.FullName)) { Writer.WriteObjectStart(); Writer.WriteValue("Path", InsertPathVariables(RuntimeDependency.Path, EngineDir, ProjectDir)); Writer.WriteValue("Type", RuntimeDependency.Type.ToString()); Writer.WriteObjectEnd(); } Writer.WriteArrayEnd(); if (PluginNameToEnabledState.Count > 0) { Writer.WriteArrayStart("Plugins"); foreach (KeyValuePair PluginNameToEnabledStatePair in PluginNameToEnabledState.OrderBy(x => x.Key)) { Writer.WriteObjectStart(); Writer.WriteValue("Name", PluginNameToEnabledStatePair.Key); Writer.WriteValue("Enabled", PluginNameToEnabledStatePair.Value); Writer.WriteObjectEnd(); } Writer.WriteArrayEnd(); } if (BuildPlugins.Count > 0) { Writer.WriteStringArrayField("BuildPlugins", BuildPlugins.OrderBy(x => x)); } if (AdditionalProperties.Count > 0) { Writer.WriteArrayStart("AdditionalProperties"); foreach (ReceiptProperty AdditionalProperty in AdditionalProperties) { Writer.WriteObjectStart(); Writer.WriteValue("Name", AdditionalProperty.Name); Writer.WriteValue("Value", AdditionalProperty.Value); Writer.WriteObjectEnd(); } Writer.WriteArrayEnd(); } Writer.WriteObjectEnd(); } } } }