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