2022-08-10 16:03:37 +00:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Generic ;
2023-05-30 18:38:07 -04:00
using System.IO ;
using System.Linq ;
using System.Text.RegularExpressions ;
2022-08-10 16:03:37 +00:00
using EpicGames.Core ;
using UnrealBuildBase ;
namespace UnrealBuildTool.ProjectFiles.Xcode
{
/// <summary>
/// Generates an Xcode project that acts as a native wrapper for an Unreal Project built as a framework.
/// </summary>
class XcodeFrameworkWrapperProject
{
2023-05-30 18:59:32 -04:00
private static readonly string PROJECT_FILE_SEARCH_EXPRESSION = "*.pbxproj" ;
private static readonly string TEMPLATE_NAME = "FrameworkWrapper" ;
private static readonly string FRAMEWORK_WRAPPER_TEMPLATE_DIRECTORY = Path . Combine ( Unreal . EngineDirectory . ToNormalizedPath ( ) , "Build" , "IOS" , "Resources" , TEMPLATE_NAME ) ;
private static readonly string TEMPLATE_PROJECT_NAME = "PROJECT_NAME" ;
private static readonly string COMMANDLINE_FILENAME = "uecommandline.txt" ;
2022-08-10 16:03:37 +00:00
/// <summary>
/// Recursively copies all of the files and directories that are inside <paramref name="SourceDirectory"/> into <paramref name="DestinationDirectory"/>.
/// </summary>
/// <param name="SourceDirectory">The directory whose contents should be copied.</param>
/// <param name="DestinationDirectory">The directory into which the files should be copied.</param>
private static void CopyAll ( string SourceDirectory , string DestinationDirectory )
{
IEnumerable < string > Directories = Directory . EnumerateDirectories ( SourceDirectory , "*" , System . IO . SearchOption . AllDirectories ) ;
// Create all the directories
foreach ( string DirSrc in Directories )
{
string DirDst = DirSrc . ToString ( ) . Replace ( SourceDirectory . ToString ( ) , DestinationDirectory . ToString ( ) ) ;
Directory . CreateDirectory ( DirDst ) ;
}
IEnumerable < string > Files = Directory . EnumerateFiles ( SourceDirectory , "*" , System . IO . SearchOption . AllDirectories ) ;
// Copy all the files
foreach ( string FileSrc in Files )
{
string FileDst = FileSrc . ToString ( ) . Replace ( SourceDirectory . ToString ( ) , DestinationDirectory . ToString ( ) ) ;
if ( ! File . Exists ( FileDst ) )
{
File . Copy ( FileSrc , FileDst ) ;
}
}
}
/// <summary>
/// An enumeration specifying the type of a filesystem entry, either directory, file, or something else.
/// </summary>
private enum EntryType { None , Directory , File }
2023-05-30 18:38:07 -04:00
2022-08-10 16:03:37 +00:00
/// <summary>
/// Gets the type of filesystem entry pointed to by <paramref name="Path"/>.
/// </summary>
/// <returns>The type of filesystem entry pointed to by <paramref name="Path"/>.</returns>
/// <param name="Path">The path to a filesystem entry.</param>
private static EntryType GetEntryType ( string Path )
{
if ( Directory . Exists ( Path ) )
{
return EntryType . Directory ;
}
else if ( File . Exists ( Path ) )
{
return EntryType . File ;
}
else
{
return EntryType . None ;
}
}
/// <summary>
/// Recursively renames all files and directories that contain <paramref name="OldValue"/> in their name by replacing
/// <paramref name="OldValue"/> with <paramref name="NewValue"/>.
/// </summary>
/// <param name="RootDirectory">Root directory.</param>
/// <param name="OldValue">Old value.</param>
/// <param name="NewValue">New value.</param>
private static void RenameFilesAndDirectories ( string RootDirectory , string OldValue , string NewValue )
{
IEnumerable < string > Entries = Directory . EnumerateFileSystemEntries ( RootDirectory , "*" , SearchOption . TopDirectoryOnly ) ;
foreach ( string Entry in Entries )
{
string NewEntryName = Path . GetFileName ( Entry ) . Replace ( OldValue , NewValue ) ;
string ParentDirectory = Path . GetDirectoryName ( Entry ) ! ;
string EntryDestination = Path . Combine ( ParentDirectory , NewEntryName ) ;
switch ( GetEntryType ( Entry ) )
{
case EntryType . Directory :
if ( Entry ! = EntryDestination )
{
Directory . Move ( Entry , EntryDestination ) ;
}
RenameFilesAndDirectories ( EntryDestination , OldValue , NewValue ) ;
break ;
case EntryType . File :
if ( Entry ! = EntryDestination )
{
File . Move ( Entry , EntryDestination ) ;
}
break ;
default :
break ;
}
}
}
2023-05-30 18:38:07 -04:00
2022-08-10 16:03:37 +00:00
/// <summary>
/// Opens each file in <paramref name="RootDirectory"/> and replaces all occurrences of <paramref name="OldValue"/>
/// with <paramref name="NewValue"/>.
/// </summary>
/// <param name="RootDirectory">The directory in which all files should be subject to replacements.</param>
2023-05-30 18:38:07 -04:00
/// <param name="SearchPattern">Only replace text in files that match this pattern. Default is all files.</param>
2022-08-10 16:03:37 +00:00
/// <param name="OldValue">The value that should be replaced in all files.</param>
/// <param name="NewValue">The replacement value.</param>
private static void ReplaceTextInFiles ( string RootDirectory , string OldValue , string NewValue , string SearchPattern = "*" )
{
IEnumerable < string > Files = Directory . EnumerateFiles ( RootDirectory , SearchPattern , SearchOption . AllDirectories ) ;
foreach ( string SrcFile in Files )
{
string FileContents = File . ReadAllText ( SrcFile ) ;
FileContents = FileContents . Replace ( OldValue , NewValue ) ;
File . WriteAllText ( SrcFile , FileContents ) ;
}
}
/// <summary>
/// Modifies the Xcode project file to change a few build settings.
/// </summary>
/// <param name="RootDirectory">The root directory of the template project that was created.</param>
/// <param name="FrameworkName">The name of the framework that this project is wrapping.</param>
/// <param name="BundleId">The Bundle ID to give to the wrapper project.</param>
/// <param name="SrcFrameworkPath">The path to the directory containing the framework to be wrapped.</param>
/// <param name="EnginePath">The path to the root Unreal Engine directory.</param>
/// <param name="CookedDataPath">The path to the 'cookeddata' folder that accompanies the framework.</param>
/// <param name="ProvisionName"></param>
/// <param name="TeamUUID"></param>
private static void SetProjectFileSettings ( string RootDirectory , string FrameworkName , string BundleId , string SrcFrameworkPath , string EnginePath , string CookedDataPath , string? ProvisionName , string? TeamUUID )
{
List < string > ProjectFiles = Directory . EnumerateFiles ( RootDirectory , PROJECT_FILE_SEARCH_EXPRESSION , SearchOption . AllDirectories ) . ToList ( ) ;
if ( ProjectFiles . Count ! = 1 )
{
throw new BuildException ( String . Format ( "Should only find 1 Xcode project file in the resources, but {0} were found." , ProjectFiles . Count ) ) ;
}
else
{
string ProjectContents = File . ReadAllText ( ProjectFiles [ 0 ] ) ;
Dictionary < string , string? > Settings = new Dictionary < string , string? > ( )
{
["FRAMEWORK_NAME"] = FrameworkName ,
["SRC_FRAMEWORK_PATH"] = SrcFrameworkPath ,
["ENGINE_PATH"] = EnginePath ,
["SRC_COOKEDDATA"] = CookedDataPath ,
["PRODUCT_BUNDLE_IDENTIFIER"] = BundleId ,
["PROVISIONING_PROFILE_SPECIFIER"] = ProvisionName ,
["DEVELOPMENT_TEAM"] = TeamUUID ,
} ;
foreach ( KeyValuePair < string , string? > Setting in Settings )
{
ProjectContents = ChangeProjectSetting ( ProjectContents , Setting . Key , Setting . Value ) ;
}
2023-05-30 18:38:07 -04:00
File . WriteAllText ( ProjectFiles [ 0 ] , ProjectContents ) ;
2022-08-10 16:03:37 +00:00
}
}
2023-05-30 18:38:07 -04:00
/// <summary>
/// Removes the readonly attribute from all files in a directory file while retaining all other attributes, thus making them writeable.
/// </summary>
/// <param name="RootDirectory">The path to the directory that will be make writeable.</param>
private static void MakeAllFilesWriteable ( string RootDirectory )
2022-08-10 16:03:37 +00:00
{
IEnumerable < string > FileNames = Directory . EnumerateFiles ( RootDirectory , "*" , SearchOption . AllDirectories ) ;
foreach ( string FileName in FileNames )
{
File . SetAttributes ( FileName , File . GetAttributes ( FileName ) & ~ FileAttributes . ReadOnly ) ;
}
}
/// <summary>
/// Changes the value of a setting in a project file.
/// </summary>
/// <returns>The project file contents with the setting replaced.</returns>
/// <param name="ProjectContents">The contents of a settings file.</param>
/// <param name="SettingName">The name of the setting to change.</param>
/// <param name="SettingValue">The new value for the setting.</param>
private static string ChangeProjectSetting ( string ProjectContents , string SettingName , string? SettingValue )
{
string SettingNameRegexString = String . Format ( "(\\s+{0}\\s=\\s)\"?(.+)\"?;" , SettingName ) ;
2023-05-30 18:38:07 -04:00
2022-08-10 16:03:37 +00:00
string SettingValueReplaceString = String . Format ( "$1\"{0}\";" , SettingValue ) ;
Regex SettingNameRegex = new Regex ( SettingNameRegexString ) ;
return SettingNameRegex . Replace ( ProjectContents , SettingValueReplaceString ) ;
}
/// <summary>
/// There are some autogenerated directories that could have accidentally made it into the template.
/// This method tries to delete those directories as an extra precaution.
/// </summary>
/// <param name="RootDirectory">The directory which should be recursively searched for unwanted directories.</param>
private static void DeleteUnwantedDirectories ( string RootDirectory )
{
HashSet < string > UnwantedDirectories = new HashSet < string > ( )
{
"Build" ,
"xcuserdata"
} ;
2023-05-30 18:38:07 -04:00
2022-08-10 16:03:37 +00:00
IEnumerable < string > Directories = Directory . EnumerateDirectories ( RootDirectory , "*" , SearchOption . AllDirectories ) ;
foreach ( string Dir in Directories )
{
string DirectoryName = Path . GetFileName ( Dir ) ;
if ( UnwantedDirectories . Contains ( DirectoryName ) & & Directory . Exists ( Dir ) )
{
Directory . Delete ( Dir , true ) ;
}
}
}
/// <summary>
/// Generates an Xcode project that acts as a native wrapper around an Unreal Project built as a framework.
///
/// Wrapper projects are generated by copying a template xcode project from the Build Resources directory,
/// deleting any user-specific or build files, renaming files and folders to match the framework, setting specific
/// settings in the project to accommodate the framework, and replacing text in all the files to match the framework.
/// </summary>
/// <param name="OutputDirectory">The directory in which to place the framework. The framework will be placed in 'outputDirectory/frameworkName/'.</param>
2023-05-30 18:38:07 -04:00
/// <param name="ProjectName">The name of the project. If blueprint-only, use the actual name of the project, not just UnrealGame.</param>
2022-08-10 16:03:37 +00:00
/// <param name="FrameworkName">The name of the framework that this project is wrapping.</param>
/// <param name="BundleId">The Bundle ID to give to the wrapper project.</param>
/// <param name="SrcFrameworkPath">The path to the directory containing the framework to be wrapped.</param>
/// <param name="EnginePath">The path to the root Unreal Engine directory.</param>
/// <param name="CookedDataPath">The path to the 'cookeddata' folder that accompanies the framework.</param>
/// <param name="ProvisionName"></param>
/// <param name="TeamUUID"></param>
public static void GenerateXcodeFrameworkWrapper ( string OutputDirectory , string ProjectName , string FrameworkName , string BundleId , string SrcFrameworkPath , string EnginePath , string CookedDataPath , string? ProvisionName , string? TeamUUID )
{
string OutputDir = Path . Combine ( OutputDirectory , FrameworkName ) ;
CopyAll ( FRAMEWORK_WRAPPER_TEMPLATE_DIRECTORY , OutputDir ) ;
DeleteUnwantedDirectories ( OutputDir ) ;
MakeAllFilesWriteable ( OutputDir ) ;
RenameFilesAndDirectories ( OutputDir , TEMPLATE_NAME , FrameworkName ) ;
SetProjectFileSettings ( OutputDir , FrameworkName , BundleId , SrcFrameworkPath , EnginePath , CookedDataPath , ProvisionName , TeamUUID ) ;
ReplaceTextInFiles ( OutputDir , TEMPLATE_NAME , FrameworkName ) ;
ReplaceTextInFiles ( OutputDir , TEMPLATE_PROJECT_NAME , ProjectName , COMMANDLINE_FILENAME ) ;
}
}
2023-05-30 18:38:07 -04:00
2022-08-10 16:03:37 +00:00
class XcodeFrameworkWrapperUtils
{
2023-05-30 18:38:07 -04:00
private static ConfigHierarchy GetIni ( DirectoryReference ProjectDirectory )
2022-08-10 16:03:37 +00:00
{
return ConfigCache . ReadHierarchy ( ConfigHierarchyType . Engine , ProjectDirectory , UnrealTargetPlatform . IOS ) ;
//return ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, DirectoryReference.FromFile(ProjectFile), UnrealTargetPlatform.IOS);
}
public static string GetBundleID ( DirectoryReference ProjectDirectory , FileReference ? ProjectFile )
{
ConfigHierarchy Ini = GetIni ( ProjectDirectory ) ;
string BundleId ;
Ini . GetString ( "/Script/IOSRuntimeSettings.IOSRuntimeSettings" , "BundleIdentifier" , out BundleId ) ;
BundleId = BundleId . Replace ( "[PROJECT_NAME]" , ( ( ProjectFile ! = null ) ? ProjectFile . GetFileNameWithoutAnyExtensions ( ) : "UnrealGame" ) ) . Replace ( "_" , "" ) ;
return BundleId ;
}
public static string GetBundleName ( DirectoryReference ProjectDirectory , FileReference ? ProjectFile )
{
ConfigHierarchy Ini = GetIni ( ProjectDirectory ) ;
string BundleName ;
Ini . GetString ( "/Script/IOSRuntimeSettings.IOSRuntimeSettings" , "BundleName" , out BundleName ) ;
BundleName = BundleName . Replace ( "[PROJECT_NAME]" , ( ( ProjectFile ! = null ) ? ProjectFile . GetFileNameWithoutAnyExtensions ( ) : "UnrealGame" ) ) . Replace ( "_" , "" ) ;
return BundleName ;
}
public static bool GetBuildAsFramework ( DirectoryReference ProjectDirectory )
{
ConfigHierarchy Ini = GetIni ( ProjectDirectory ) ;
bool bBuildAsFramework ;
Ini . GetBool ( "/Script/IOSRuntimeSettings.IOSRuntimeSettings" , "bBuildAsFramework" , out bBuildAsFramework ) ;
return bBuildAsFramework ;
}
2023-05-30 18:38:07 -04:00
2022-08-10 16:03:37 +00:00
public static bool GetGenerateFrameworkWrapperProject ( DirectoryReference ProjectDirectory )
{
ConfigHierarchy Ini = GetIni ( ProjectDirectory ) ;
bool bGenerateFrameworkWrapperProject ;
Ini . GetBool ( "/Script/IOSRuntimeSettings.IOSRuntimeSettings" , "bGenerateFrameworkWrapperProject" , out bGenerateFrameworkWrapperProject ) ;
return bGenerateFrameworkWrapperProject ;
}
}
2023-05-30 18:38:07 -04:00
}