// Copyright Epic Games, Inc. All Rights Reserved. using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Xml; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Represents a Mac/IOS framework /// class UEBuildFramework { /// /// The name of this framework /// public readonly string Name; /// /// Path to a zip file containing the framework. May be null. /// public readonly FileReference? ZipFile; /// /// Path where the zip should be extracted. May be null. /// public readonly DirectoryReference? ZipOutputDirectory; /// /// Base path to the framework on disk. /// readonly DirectoryReference? FrameworkDirectory; /// /// /// public readonly string? CopyBundledAssets; /// /// Link the framework libraries into the executable /// public readonly bool bLinkFramework = true; /// /// Copy the framework to the target's Framework directory /// public readonly bool bCopyFramework = false; /// /// File created after the framework has been extracted. Used to add dependencies into the action graph, used by Modern Xcode as well /// public FileItem? ExtractedTokenFile => ZipOutputDirectory == null ? null : FileItem.GetItemByFileReference(new FileReference(ZipOutputDirectory!.FullName + ".extracted")); /// /// For legacy xcode projects, we unzip in UBT (isntead of xcode), so we track if we've made an action for this framework yet (if two /// modules use the same framework, we only want to unzip it once. Other than time waste, there could be conflicts /// public bool bHasMadeUnzipAction; /// /// List of variants contained in this XCFramework. Only non null for XCFrameworks /// readonly List? XCFrameworkVariants; /// /// Constructor /// /// The framework name /// public UEBuildFramework(string Name, string? CopyBundledAssets = null) { this.Name = Name; this.CopyBundledAssets = CopyBundledAssets; } /// /// Constructor /// /// The framework name /// Path to the zip file for this framework/xcframework /// Path for the extracted zip file /// /// Link the framework into the executable /// Copy the framework to the target's Framework directory /// Logger for diagnostic output public UEBuildFramework(string Name, FileReference? ZipFile, DirectoryReference OutputDirectory, string? CopyBundledAssets, bool bLinkFramework, bool bCopyFramework, ILogger Logger) { this.Name = Name; this.ZipFile = ZipFile; ZipOutputDirectory = OutputDirectory; FrameworkDirectory = OutputDirectory; this.CopyBundledAssets = CopyBundledAssets; this.bCopyFramework = bCopyFramework; this.bLinkFramework = bLinkFramework; if (this.ZipFile != null && this.ZipFile.FullName.EndsWith(".xcframework.zip")) { XCFrameworkVariants = LoadXCFrameworkVariantsFromZipFile(Logger); } } /// /// Constructor /// /// The framework name /// Path for the framework/xcframework on disk /// /// Link the framework into the executable /// Copy the framework to the target's Framework directory /// Logger for diagnostic output public UEBuildFramework(string Name, DirectoryReference FrameworkDirectory, string? CopyBundledAssets, bool bLinkFramework, bool bCopyFramework, ILogger Logger) { this.Name = Name; this.FrameworkDirectory = FrameworkDirectory; this.CopyBundledAssets = CopyBundledAssets; this.bCopyFramework = bCopyFramework; this.bLinkFramework = bLinkFramework; if (this.FrameworkDirectory.FullName.EndsWith(".xcframework")) { XCFrameworkVariants = LoadXCFrameworkVariants(Logger); } } /// /// Gets the framework directory for given build settings /// /// /// /// Logger for diagnostic output public DirectoryReference? GetFrameworkDirectory(UnrealTargetPlatform? Platform, UnrealArch? Architecture, ILogger Logger) { if (XCFrameworkVariants != null && Platform != null && Architecture != null) { XCFrameworkVariantEntry? FrameworkVariant = XCFrameworkVariants.Find(x => x.Matches(Platform.Value, Architecture.Value)); if (FrameworkVariant != null) { return DirectoryReference.Combine(FrameworkDirectory!, FrameworkVariant.LibraryIdentifier); } Logger.LogWarning("Variant for platform \"{Platform}\" with architecture {Architecture} not found in XCFramework {Name}.", Platform.ToString(), Architecture, Name); } return FrameworkDirectory; } /// /// Loads XCFramework variants description from Info.plist file inside XCFramework structure /// Logger for diagnostic output /// List? LoadXCFrameworkVariants(ILogger Logger) { XmlDocument PlistDoc = new XmlDocument(); PlistDoc.Load(FileReference.Combine(FrameworkDirectory!, "Info.plist").FullName); return LoadXCFrameworkVariants(PlistDoc, Logger); } /// /// Loads XCFramework variants description from Info.plist file inside the zipped XCFramework structure /// Logger for diagnostic output /// List? LoadXCFrameworkVariantsFromZipFile(ILogger Logger) { using (ZipArchive Archive = System.IO.Compression.ZipFile.OpenRead(ZipFile!.FullName)) { ZipArchiveEntry? Entry = Archive.GetEntry($"{Name}.xcframework/Info.plist"); if (Entry == null) { Logger.LogError("Failed find Info.plist in XCFramework {Name}", Name); return null; } else { Stream InfoPlist = Entry.Open(); XmlDocument PlistDoc = new XmlDocument(); PlistDoc.Load(InfoPlist); return LoadXCFrameworkVariants(PlistDoc, Logger); } } } /// /// Loads XCFramework variants description from Info.plist inside XCFramework structure loaded in a XmlDocument /// XmlDocument containing Info.plist that defines the xcframework settings /// Logger for diagnostic output /// List? LoadXCFrameworkVariants(XmlDocument PlistDoc, ILogger Logger) { // Check the plist type XmlNode? CFBundlePackageType = PlistDoc.SelectSingleNode("/plist/dict[key='CFBundlePackageType']/string[1]"); if (CFBundlePackageType == null || CFBundlePackageType.NodeType != XmlNodeType.Element || CFBundlePackageType.InnerText != "XFWK") { Logger.LogWarning("CFBundlePackageType is not set to XFWK in Info.plist for XCFramework {Name}", Name); return null; } // Load Framework variants data from dictionary nodes XmlNodeList? FrameworkVariantDicts = PlistDoc.SelectNodes("/plist/dict[key='AvailableLibraries']/array/dict"); if (FrameworkVariantDicts == null) { Logger.LogWarning("Invalid Info.plist file for XCFramework {Name}. It will be used as a regular Framework", Name); return null; } List Variants = new List(); foreach (XmlNode VariantDict in FrameworkVariantDicts) { XCFrameworkVariantEntry? Variant = XCFrameworkVariantEntry.Parse(VariantDict, Logger); if (Variant == null) { Logger.LogWarning("Failed to load variant from Info.plist file for XCFramework {Name}", Name); } else { Logger.LogTrace("Found {LibraryIdentifier} variant in XCFramework {Name}", Variant.LibraryIdentifier, Name); Variants.Add(Variant); } } return Variants; } /// /// Represents a XCFramework variant description /// private class XCFrameworkVariantEntry { /// /// Identifier of this framework variant. Is in the form platform-architectures-platformvariant. /// Some examples would be ios-arm64, ios-arm64_x86_64-simulator /// It also represents the relative path inside the XCFramefork where the Framework for this variant resides /// public readonly string LibraryIdentifier; /// /// Relative path where framework lives after applying LibraryIdentifier path /// public readonly string LibraryPath; /// /// List of supported architectures for this Framework variants /// public readonly List SupportedArchitectures; /// /// Platform this Framework variant is intended for. Possible values are ios, macos, watchos, tvos /// public readonly string SupportedPlatform; /// /// Platform variant for this Framework variant. It can be empty or any other value representing a platform variane, like maccatalyst or simulator /// public readonly string? SupportedPlatformVariant; /// /// Constructor /// /// /// /// /// /// XCFrameworkVariantEntry(string LibraryIdentifier, string LibraryPath, List SupportedArchitectures, string SupportedPlatform, string? SupportedPlatformVariant) { this.LibraryIdentifier = LibraryIdentifier; this.LibraryPath = LibraryPath; this.SupportedArchitectures = SupportedArchitectures; this.SupportedPlatform = SupportedPlatform; this.SupportedPlatformVariant = SupportedPlatformVariant; } /// /// Returns wether this variant is a match for given platform and architecture /// /// /// public bool Matches(UnrealTargetPlatform Platform, UnrealArch Architecture) { if ((Platform == UnrealTargetPlatform.IOS && SupportedPlatform == "ios") || (Platform == UnrealTargetPlatform.Mac && SupportedPlatform == "macos") || (Platform == UnrealTargetPlatform.TVOS && SupportedPlatform == "tvos")) { if (Architecture == UnrealArch.IOSSimulator || Architecture == UnrealArch.TVOSSimulator) { // When using -simulator we don't have the actual architecture. Assume arm64 as other parts of UBT already are doing return (SupportedPlatformVariant == "simulator") && SupportedArchitectures.Contains("arm64"); } else { return (SupportedPlatformVariant == null) && SupportedArchitectures.Contains(Architecture.AppleName); } } return false; } /// /// Creates a XCFrameworkVariantEntry by parsing the content from the XCFramework Info.plist file /// /// XmlNode loaded form Info.plist containing the representation of a <dict> that contains the variant description /// Logger for diagnostic output public static XCFrameworkVariantEntry? Parse(XmlNode DictNode, ILogger Logger) { // Entries from a in a plist file always contains a node and a value node if ((DictNode.ChildNodes.Count % 2) != 0) { Logger.LogDebug("Invalid dict in Info.plist file for XCFramework"); return null; } string? LibraryIdentifier = null; string? LibraryPath = null; List? SupportedArchitectures = null; string? SupportedPlatform = null; string? SupportedPlatformVariant = null; for (int i = 0; i < DictNode.ChildNodes.Count; i += 2) { XmlNode? Key = DictNode.ChildNodes[i]; XmlNode? Value = DictNode.ChildNodes[i + 1]; if (Value != null && Key != null && Key.Name == "key") { switch (Key.InnerText) { case "LibraryIdentifier": LibraryIdentifier = ParseString(Value, Logger); break; case "LibraryPath": LibraryPath = ParseString(Value, Logger); break; case "SupportedPlatform": SupportedPlatform = ParseString(Value, Logger); break; case "SupportedPlatformVariant": SupportedPlatformVariant = ParseString(Value, Logger); break; case "SupportedArchitectures": SupportedArchitectures = ParseListOfStrings(Value, Logger); break; default: break; } } } // All fields but SupportedPlatformVariant are allowed to be present in the framework variant descriprion if (LibraryIdentifier == null || LibraryPath == null || SupportedArchitectures == null || SupportedPlatform == null) { Logger.LogDebug("Missing data in Info.plist file for XCFramework"); return null; } return new XCFrameworkVariantEntry(LibraryIdentifier, LibraryPath, SupportedArchitectures, SupportedPlatform, SupportedPlatformVariant); } /// /// Parses the content from a string value node in a plist file /// /// XmlNode we expect to contain a <string> value /// Logger for diagnostic output static string? ParseString(XmlNode ValueNode, ILogger Logger) { if (ValueNode.Name != "string") { Logger.LogDebug("Unexpected tag \"{Tag}\" while expecting \"string\" in Info.plist file for XCFramework", ValueNode.Name); return null; } return ValueNode.InnerText; } /// /// Parses the content from an array value node in a plist file that has several string entries /// /// XmlNode we expect to contain a <array> value with several <string> entries /// Logger for diagnostic output static List? ParseListOfStrings(XmlNode ValueNode, ILogger Logger) { if (ValueNode.Name != "array") { Logger.LogDebug("Unexpected tag \"{Name}\" while expecting \"array\" in Info.plist file for XCFramework", ValueNode.Name); return null; } List ListOfStrings = new List(); foreach (XmlNode ChildNode in ValueNode.ChildNodes) { string? ParsedString = ParseString(ChildNode, Logger); if (ParsedString != null) { ListOfStrings.Add(ParsedString); } } return ListOfStrings; } } } }