// 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;
}
}
}
}