// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using EpicGames.Core;
using UnrealBuildBase;
namespace UnrealBuildTool.ProjectFiles.Xcode
{
///
/// Generates an Xcode project that acts as a native wrapper for an Unreal Project built as a framework.
///
class XcodeFrameworkWrapperProject
{
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";
///
/// Recursively copies all of the files and directories that are inside into .
///
/// The directory whose contents should be copied.
/// The directory into which the files should be copied.
private static void CopyAll(string SourceDirectory, string DestinationDirectory)
{
IEnumerable 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 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);
}
}
}
///
/// An enumeration specifying the type of a filesystem entry, either directory, file, or something else.
///
private enum EntryType { None, Directory, File }
///
/// Gets the type of filesystem entry pointed to by .
///
/// The type of filesystem entry pointed to by .
/// The path to a filesystem entry.
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;
}
}
///
/// Recursively renames all files and directories that contain in their name by replacing
/// with .
///
/// Root directory.
/// Old value.
/// New value.
private static void RenameFilesAndDirectories(string RootDirectory, string OldValue, string NewValue)
{
IEnumerable 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;
}
}
}
///
/// Opens each file in and replaces all occurrences of
/// with .
///
/// The directory in which all files should be subject to replacements.
/// Only replace text in files that match this pattern. Default is all files.
/// The value that should be replaced in all files.
/// The replacement value.
private static void ReplaceTextInFiles(string RootDirectory, string OldValue, string NewValue, string SearchPattern = "*")
{
IEnumerable 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);
}
}
///
/// Modifies the Xcode project file to change a few build settings.
///
/// The root directory of the template project that was created.
/// The name of the framework that this project is wrapping.
/// The Bundle ID to give to the wrapper project.
/// The path to the directory containing the framework to be wrapped.
/// The path to the root Unreal Engine directory.
/// The path to the 'cookeddata' folder that accompanies the framework.
///
///
private static void SetProjectFileSettings(string RootDirectory, string FrameworkName, string BundleId, string SrcFrameworkPath, string EnginePath, string CookedDataPath, string? ProvisionName, string? TeamUUID)
{
List 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 Settings = new Dictionary()
{
["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 Setting in Settings)
{
ProjectContents = ChangeProjectSetting(ProjectContents, Setting.Key, Setting.Value);
}
File.WriteAllText(ProjectFiles[0], ProjectContents);
}
}
///
/// Removes the readonly attribute from all files in a directory file while retaining all other attributes, thus making them writeable.
///
/// The path to the directory that will be make writeable.
private static void MakeAllFilesWriteable(string RootDirectory)
{
IEnumerable FileNames = Directory.EnumerateFiles(RootDirectory, "*", SearchOption.AllDirectories);
foreach (string FileName in FileNames)
{
File.SetAttributes(FileName, File.GetAttributes(FileName) & ~FileAttributes.ReadOnly);
}
}
///
/// Changes the value of a setting in a project file.
///
/// The project file contents with the setting replaced.
/// The contents of a settings file.
/// The name of the setting to change.
/// The new value for the setting.
private static string ChangeProjectSetting(string ProjectContents, string SettingName, string? SettingValue)
{
string SettingNameRegexString = String.Format("(\\s+{0}\\s=\\s)\"?(.+)\"?;", SettingName);
string SettingValueReplaceString = String.Format("$1\"{0}\";", SettingValue);
Regex SettingNameRegex = new Regex(SettingNameRegexString);
return SettingNameRegex.Replace(ProjectContents, SettingValueReplaceString);
}
///
/// 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.
///
/// The directory which should be recursively searched for unwanted directories.
private static void DeleteUnwantedDirectories(string RootDirectory)
{
HashSet UnwantedDirectories = new HashSet()
{
"Build",
"xcuserdata"
};
IEnumerable 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);
}
}
}
///
/// 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.
///
/// The directory in which to place the framework. The framework will be placed in 'outputDirectory/frameworkName/'.
/// The name of the project. If blueprint-only, use the actual name of the project, not just UnrealGame.
/// The name of the framework that this project is wrapping.
/// The Bundle ID to give to the wrapper project.
/// The path to the directory containing the framework to be wrapped.
/// The path to the root Unreal Engine directory.
/// The path to the 'cookeddata' folder that accompanies the framework.
///
///
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);
}
}
class XcodeFrameworkWrapperUtils
{
private static ConfigHierarchy GetIni(DirectoryReference ProjectDirectory)
{
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;
}
public static bool GetGenerateFrameworkWrapperProject(DirectoryReference ProjectDirectory)
{
ConfigHierarchy Ini = GetIni(ProjectDirectory);
bool bGenerateFrameworkWrapperProject;
Ini.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bGenerateFrameworkWrapperProject", out bGenerateFrameworkWrapperProject);
return bGenerateFrameworkWrapperProject;
}
}
}