// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using AutomationTool; using UnrealBuildTool; using System.Text.RegularExpressions; using Ionic.Zip; using Ionic.Zlib; using System.Security.Principal; using System.Threading; using System.Diagnostics; using EpicGames.Core; using System.Xml; using UnrealBuildBase; static class IOSEnvVarNames { // Should we code sign when staging? (defaults to 1 if not present) static public readonly string CodeSignWhenStaging = "uebp_CodeSignWhenStaging"; } class IOSClientProcess : IProcessResult { private IProcessResult childProcess; private Thread consoleLogWorker; //private bool processConsoleLogs; public IOSClientProcess(IProcessResult inChildProcess, string inDeviceID) { childProcess = inChildProcess; // Startup another thread that collect device console logs //processConsoleLogs = true; consoleLogWorker = new Thread(() => ProcessConsoleOutput(inDeviceID)); consoleLogWorker.Start(); } public void StopProcess(bool KillDescendants = true) { childProcess.StopProcess(KillDescendants); StopConsoleOutput(); } public bool HasExited { get { bool result = childProcess.HasExited; if (result) { StopConsoleOutput(); } return result; } } public string GetProcessName() { return childProcess.GetProcessName(); } public void OnProcessExited() { childProcess.OnProcessExited(); StopConsoleOutput(); } public void DisposeProcess() { childProcess.DisposeProcess(); } public void StdOut(object sender, DataReceivedEventArgs e) { childProcess.StdOut(sender, e); } public void StdErr(object sender, DataReceivedEventArgs e) { childProcess.StdErr(sender, e); } public int ExitCode { get { return childProcess.ExitCode; } set { childProcess.ExitCode = value; } } public string Output { get { return childProcess.Output; } } public Process ProcessObject { get { return childProcess.ProcessObject; } } public new string ToString() { return childProcess.ToString(); } public void WaitForExit() { childProcess.WaitForExit(); } public FileReference WriteOutputToFile(string FileName) { return childProcess.WriteOutputToFile(FileName); } private void StopConsoleOutput() { //processConsoleLogs = false; consoleLogWorker.Join(); } public void ProcessConsoleOutput(string inDeviceID) { // MobileDeviceInstance targetDevice = null; // foreach(MobileDeviceInstance curDevice in MobileDeviceInstanceManager.GetSnapshotInstanceList()) // { // if(curDevice.DeviceId == inDeviceID) // { // targetDevice = curDevice; // break; // } // } // // if(targetDevice == null) // { // return; // } // // targetDevice.StartSyslogService(); // // while(processConsoleLogs) // { // string logData = targetDevice.GetSyslogData(); // // Console.WriteLine("DeviceLog: " + logData); // } // // targetDevice.StopSyslogService(); } } public class IOSPlatform : ApplePlatform { bool bCreatedIPA = false; private string PlatformName = null; private string SDKName = null; public IOSPlatform() : this(UnrealTargetPlatform.IOS) { } public IOSPlatform(UnrealTargetPlatform TargetPlatform) : base(TargetPlatform) { PlatformName = TargetPlatform.ToString(); SDKName = (TargetPlatform == UnrealTargetPlatform.TVOS) ? "appletvos" : "iphoneos"; } public override bool GetDeviceUpdateSoftwareCommand(out string Command, out string Params, ref bool bRequiresPrivilegeElevation, ref bool bCreateWindow, ITurnkeyContext TurnkeyContext, DeviceInfo Device) { if (UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac) { Command = Params = null; return true; } TurnkeyContext.Log("Installing an offline downloaded .ipsw onto your device using the Apple Configurator application."); // cfgtool needs ECID, not UDID, so find it string Configurator = Path.Combine(GetConfiguratorLocation().Replace(" ", "\\ "), "Contents/MacOS/cfgutil"); string CfgUtilParams = string.Format("-c '{0} list | grep {1}'", Configurator, Device.Id); string CfgUtilOutput = UnrealBuildTool.Utils.RunLocalProcessAndReturnStdOut("sh", CfgUtilParams); bRequiresPrivilegeElevation = false; Match Result = Regex.Match(CfgUtilOutput, @"Type: (\S*).*ECID: (\S*)"); if (!Result.Success) { TurnkeyContext.ReportError($"Unable to find the given deviceid: {Device} in cfgutil output"); Command = Params = null; return false; } Command = "sh"; Params = string.Format("-c '{0} --ecid {1} update --ipsw $(CopyOutputPath)'", Configurator, Result.Groups[2]); return true; } private class VerifyIOSSettings { public string CodeSigningIdentity = null; public string BundleId = null; public string Account = null; public string Password = null; public string Team = null; public string Provision = null; public string RubyScript = Path.Combine(Unreal.EngineDirectory.FullName, "Build/Turnkey/VerifyIOS.ru"); public string InstallCertScript = Path.Combine(Unreal.EngineDirectory.FullName, "Build/Turnkey/InstallCert.ru"); private ITurnkeyContext TurnkeyContext; public VerifyIOSSettings(BuildCommand Command, ITurnkeyContext TurnkeyContext) { this.TurnkeyContext = TurnkeyContext; FileReference ProjectPath = Command.ParseProjectParam(); string ProjectName = ProjectPath == null ? "" : ProjectPath.GetFileNameWithoutAnyExtensions(); ConfigHierarchy EngineConfig = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, ProjectPath == null ? null : ProjectPath.Directory, UnrealTargetPlatform.IOS); // first look for settings on the commandline: CodeSigningIdentity = Command.ParseParamValue("certificate"); BundleId = Command.ParseParamValue("bundleid"); Account = Command.ParseParamValue("devcenterusername"); Password = Command.ParseParamValue("devcenterpassword"); Team = Command.ParseParamValue("teamid"); Provision = Command.ParseParamValue("provision"); if (string.IsNullOrEmpty(Team)) Team = TurnkeyContext.GetVariable("User_AppleDevCenterTeamID"); if (string.IsNullOrEmpty(Account)) Account = TurnkeyContext.GetVariable("User_AppleDevCenterUsername"); if (string.IsNullOrEmpty(Provision)) Provision = TurnkeyContext.GetVariable("User_IOSProvisioningProfile"); // fall back to ini for anything else if (string.IsNullOrEmpty(CodeSigningIdentity)) EngineConfig.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "DevCodeSigningIdentity", out CodeSigningIdentity); if (string.IsNullOrEmpty(BundleId)) EngineConfig.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "BundleIdentifier", out BundleId); if (string.IsNullOrEmpty(Team)) EngineConfig.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "IOSTeamID", out Team); if (string.IsNullOrEmpty(Account)) EngineConfig.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "DevCenterUsername", out Account); if (string.IsNullOrEmpty(Password)) EngineConfig.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "DevCenterPassword", out Password); if (string.IsNullOrEmpty(Provision)) EngineConfig.GetString("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "MobileProvision", out Provision); BundleId = BundleId.Replace("[PROJECT_NAME]", ProjectName); // some are required if (string.IsNullOrEmpty(BundleId)) { throw new AutomationException("Turnkey IOS verification requires bundle id (have '{1}', ex: com.company.foo)", CodeSigningIdentity, BundleId); } } public bool RunCommandMaybeInteractive(string Command, string Params, bool bInteractive) { Console.WriteLine("Running Command '{0} {1}'", Command, Params); int ExitCode; // if non-interactive, we can just run directly in the current shell if (!bInteractive) { UnrealBuildTool.Utils.RunLocalProcessAndReturnStdOut(Command, Params, Log.Logger, out ExitCode); } else { // otherwise, run in a new Terminal window via AppleScript string ReturnCodeFilename = Path.GetTempFileName(); // run potentially interactive scripts in a Terminal window Params = string.Format( " -e \"tell application \\\"Finder\\\"\"" + " -e \"set desktopBounds to bounds of window of desktop\"" + " -e \"end tell\"" + " -e \"tell application \\\"Terminal\\\"\"" + " -e \"activate\"" + " -e \"set newTab to do script (\\\"{3}; {0} {1}; echo $? > {2}; {3}; exit\\\")\"" + " -e \"set newWindow to window 1\"" + " -e \"set size of newWindow to {{ item 3 of desktopBounds / 2, item 4 of desktopBounds / 2 }}\"" + " -e \"repeat\"" + " -e \"delay 1\"" + " -e \"if not busy of newTab then exit repeat\"" + " -e \"end repeat\"" + " -e \"set exitCode to item 1 of paragraphs of (read \\\"{2}\\\")\"" + " -e \"if exitCode is equal to \\\"0\\\" then\"" + " -e \"close newWindow\"" + " -e \"end if\"" + " -e \"end tell\"", Command, Params.Replace("\"", "\\\\\\\""), ReturnCodeFilename, "printf \\\\\\\"\\\\\\n\\\\\\n\\\\\\n\\\\\\n\\\\\\\""); Console.WriteLine("\n\n\n{0}\n\n\n", Params); UnrealBuildTool.Utils.RunLocalProcessAndReturnStdOut("osascript", Params, Log.Logger, out ExitCode); if (ExitCode == 0) { ExitCode = int.Parse(File.ReadAllText(ReturnCodeFilename)); File.Delete(ReturnCodeFilename); } } if (ExitCode != 0) { // only ExitCode 3 (needs cert) can be handled. Any other error can't be fixed (Interactive means it can't be fixed) if (!bInteractive || ExitCode != 3) { if (ExitCode == 3) { TurnkeyContext.ReportError("Signing certificate is required."); } else { // @todo turnkey: turn exitcodes into useful messages TurnkeyContext.ReportError($"Ruby command exited with code {ExitCode}"); } return false; } // only here with ExitCode 3 if (!InstallCert()) { TurnkeyContext.ReportError($"Certificate installation failed."); return false; } } return ExitCode == 0; } public bool RunRubyCommand(bool bVerifyOnly, string DeviceName) { string Params; Params = string.Format("--bundleid {0}", BundleId); if (!string.IsNullOrEmpty(CodeSigningIdentity)) { Params += string.Format(" --identity \"{0}\"", CodeSigningIdentity); } if (!string.IsNullOrEmpty(Account)) { Params += string.Format(" --login {0}", Account); } if (!string.IsNullOrEmpty(Password)) { Params += string.Format(" --password {0}", Password); } if (!string.IsNullOrEmpty(Team)) { Params += string.Format(" --team {0}", Team); } if (!string.IsNullOrEmpty(Provision)) { Params += string.Format(" --provision {0}", Provision); } if (!string.IsNullOrEmpty(DeviceName)) { Params += string.Format(" --device {0}", DeviceName); } if (bVerifyOnly) { Params += string.Format(" --verifyonly"); } return RunCommandMaybeInteractive(RubyScript, Params, !bVerifyOnly); } private bool InstallCert() { // string ProjectName = TurnkeyContext.GetVariable("Project"); string CertLoc = null; if (!string.IsNullOrEmpty(BundleId)) { CertLoc = TurnkeyContext.RetrieveFileSource("DevCert: " + BundleId); } if (CertLoc == null) { CertLoc = TurnkeyContext.RetrieveFileSource("DevCert"); } if (CertLoc != null) { // get the cert password from Studio settings string CertPassword = TurnkeyContext.GetVariable("Studio_AppleSigningCertPassword"); TurnkeyContext.Log($"Will install cert from: '{CertLoc}'"); // osascript -e 'Tell application "System Events" to display dialog "Enter the network password:" with hidden answer default answer ""' -e 'text returned of result' 2>/dev/null string CommandLine = string.Format("'{0}' '{1}'", CertLoc, CertPassword); // run ruby script to install cert return RunCommandMaybeInteractive(InstallCertScript, CommandLine, true); } else { TurnkeyContext.ReportError("Unable to find a tagged source for DevCert"); return false; } } } string GetConfiguratorLocation() { string FindCommand = "-c 'mdfind \"kMDItemKind == Application\" | grep \"Apple Configurator 2.app\"'"; return UnrealBuildTool.Utils.RunLocalProcessAndReturnStdOut("sh", FindCommand); } //Disabling for 5.0 early access as this code was not executing and has not been tested. /* public override bool UpdateHostPrerequisites(BuildCommand Command, ITurnkeyContext TurnkeyContext, bool bVerifyOnly) { int ExitCode; if (HostPlatform.Current.HostEditorPlatform != UnrealTargetPlatform.Mac) { return base.UpdateHostPrerequisites(Command, TurnkeyContext, bVerifyOnly); } // make sure the Configurator is installed string ConfiguratorLocation = GetConfiguratorLocation(); if (ConfiguratorLocation == "") { if (bVerifyOnly) { TurnkeyContext.ReportError("Apple Configurator 2 is required."); return false; } TurnkeyContext.PauseForUser("Apple Configurator 2 is required for some automation to work. You should install it from the App Store. Launching..."); // we need to install Configurator 2, and we will block until it's done UnrealBuildTool.Utils.RunLocalProcessAndReturnStdOut("open", "macappstore://apps.apple.com/us/app/apple-configurator-2/id1037126344?mt=12"); while ((ConfiguratorLocation = GetConfiguratorLocation()) == "") { Thread.Sleep(1000); } } string IsFastlaneInstalled = UnrealBuildTool.Utils.RunLocalProcessAndReturnStdOut("/usr/bin/gem", "list -ie fastlane"); if (IsFastlaneInstalled != "true") { Console.WriteLine("Fastlane is not installed"); if (bVerifyOnly) { TurnkeyContext.ReportError("Fastlane is not installed."); return false; } TurnkeyContext.PauseForUser("Installing Fastlane from internet source. You may ignore the error about the bin directory not in your path."); // install missing fastlane without needing sudo UnrealBuildTool.Utils.RunLocalProcessAndReturnStdOut("/usr/bin/gem", "install fastlane --user-install --no-document", out ExitCode, true); if (ExitCode != 0) { return false; } } VerifyIOSSettings Settings = new VerifyIOSSettings(Command, TurnkeyContext); // look if we have a cert that matches it return Settings.RunRubyCommand(bVerifyOnly, null); } public override bool UpdateDevicePrerequisites(DeviceInfo Device, BuildCommand Command, ITurnkeyContext TurnkeyContext, bool bVerifyOnly) { if (HostPlatform.Current.HostEditorPlatform != UnrealTargetPlatform.Mac) { return base.UpdateDevicePrerequisites(Device, Command, TurnkeyContext, bVerifyOnly); } VerifyIOSSettings Settings = new VerifyIOSSettings(Command, TurnkeyContext); // @todo turnkey - better to use the device's udid if it's set properly in DeviceInfo string DeviceName = Device == null ? null : Device.Name; // now look for a provision that can be used with a (maybe newly) instally cert return Settings.RunRubyCommand(bVerifyOnly, DeviceName); } */ public override DeviceInfo[] GetDevices() { List Devices = new List(); var IdeviceIdPath = GetPathToLibiMobileDeviceTool("idevice_id"); string Output = Utils.RunLocalProcessAndReturnStdOut(IdeviceIdPath, ""); var ConnectedDevicesUDIDs = Output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); foreach (string UnparsedUDID in ConnectedDevicesUDIDs) { DeviceInfo CurrentDevice = new DeviceInfo(TargetPlatformType); var IdeviceInfoPath = GetPathToLibiMobileDeviceTool("ideviceinfo"); String ParsedUDID = UnparsedUDID.Split(" ").First(); String IdeviceInfoArgs = "-u " + ParsedUDID; if (UnparsedUDID.Contains("Network")) { CurrentDevice.PlatformValues["Connection"] = "Network"; IdeviceInfoArgs = "-n " + IdeviceInfoArgs; } else { CurrentDevice.PlatformValues["Connection"] = "USB"; } string OutputInfo = Utils.RunLocalProcessAndReturnStdOut(IdeviceInfoPath, IdeviceInfoArgs); foreach (string Line in OutputInfo.Split(Environment.NewLine.ToCharArray())) { // check we are returning the proper device for this class if (Line.StartsWith("DeviceClass:")) { bool bIsDeviceTVOS = Line.Split(": ").Last().ToLower() == "tvos"; if (bIsDeviceTVOS != (TargetPlatformType == UnrealTargetPlatform.TVOS)) { Devices.Remove(CurrentDevice); } } else if (Line.StartsWith("DeviceName: ")) { CurrentDevice.Name = Line.Split(": ").Last(); } else if (Line.StartsWith("UniqueDeviceID: ")) { CurrentDevice.Id = Line.Split(": ").Last(); } else if (Line.StartsWith("ProductType: ")) { CurrentDevice.Type = Line.Split(": ").Last(); } else if (Line.StartsWith("ProductVersion: ")) { CurrentDevice.SoftwareVersion = Line.Split(": ").Last(); } } Devices.Add(CurrentDevice); } return Devices.ToArray(); } public override string GetPlatformPakCommandLine(ProjectParams Params, DeploymentContext SC) { string PakParams = ""; string OodleDllPath = DirectoryReference.Combine(SC.ProjectRoot, "Binaries/ThirdParty/Oodle/Mac/libUnrealPakPlugin.dylib").FullName; if (File.Exists(OodleDllPath)) { PakParams += String.Format(" -customcompressor=\"{0}\"", OodleDllPath); } return PakParams; } public virtual bool PrepForUATPackageOrDeploy(UnrealTargetConfiguration Config, FileReference ProjectFile, string InProjectName, DirectoryReference InProjectDirectory, string InExecutablePath, DirectoryReference InEngineDir, bool bForDistribution, string CookFlavor, bool bIsDataDeploy, bool bCreateStubIPA, bool bIsUEGame) { FileReference TargetReceiptFileName = GetTargetReceiptFileName(Config, InExecutablePath, InEngineDir, InProjectDirectory, bIsUEGame); return IOSExports.PrepForUATPackageOrDeploy(Config, ProjectFile, InProjectName, InProjectDirectory, InExecutablePath, InEngineDir, bForDistribution, CookFlavor, bIsDataDeploy, bCreateStubIPA, TargetReceiptFileName, Log.Logger); } private FileReference GetTargetReceiptFileName(UnrealTargetConfiguration Config, string InExecutablePath, DirectoryReference InEngineDir, DirectoryReference InProjectDirectory, bool bIsUEGame) { string TargetName = Path.GetFileNameWithoutExtension(InExecutablePath).Split("-".ToCharArray())[0]; FileReference TargetReceiptFileName; if (bIsUEGame) { TargetReceiptFileName = TargetReceipt.GetDefaultPath(InEngineDir, "UnrealGame", UnrealTargetPlatform.IOS, Config, ""); } else { TargetReceiptFileName = TargetReceipt.GetDefaultPath(InProjectDirectory, TargetName, UnrealTargetPlatform.IOS, Config, ""); } return TargetReceiptFileName; } public virtual void GetProvisioningData(FileReference InProject, bool bDistribution, out string MobileProvision, out string SigningCertificate, out string TeamUUID, out bool bAutomaticSigning) { IOSExports.GetProvisioningData(InProject, bDistribution, out MobileProvision, out SigningCertificate, out TeamUUID, out bAutomaticSigning); } public virtual bool DeployGeneratePList(FileReference ProjectFile, UnrealTargetConfiguration Config, DirectoryReference ProjectDirectory, bool bIsUEGame, string GameName, bool bIsClient, string ProjectName, DirectoryReference InEngineDir, DirectoryReference AppDirectory, string InExecutablePath) { FileReference TargetReceiptFileName = GetTargetReceiptFileName(Config, InExecutablePath, InEngineDir, ProjectDirectory, bIsUEGame); return IOSExports.GeneratePList(ProjectFile, Config, ProjectDirectory, bIsUEGame, GameName, bIsClient, ProjectName, InEngineDir, AppDirectory, TargetReceiptFileName, Log.Logger); } protected string MakeIPAFileName(UnrealTargetConfiguration TargetConfiguration, ProjectParams Params, DeploymentContext SC, bool bAllowDistroPrefix) { string ExeName = SC.StageExecutables[0]; if (!SC.IsCodeBasedProject) { ExeName = ExeName.Replace("UnrealGame", Params.RawProjectPath.GetFileNameWithoutExtension()); } return Path.Combine(Path.GetDirectoryName(Params.RawProjectPath.FullName), "Binaries", PlatformName, ((bAllowDistroPrefix && Params.Distribution) ? "Distro_" : "") + ExeName + ".ipa"); } // Determine if we should code sign protected bool GetCodeSignDesirability(ProjectParams Params) { //@TODO: Would like to make this true, as it's the common case for everyone else bool bDefaultNeedsSign = true; bool bNeedsSign = false; string EnvVar = InternalUtils.GetEnvironmentVariable(IOSEnvVarNames.CodeSignWhenStaging, bDefaultNeedsSign ? "1" : "0", /*bQuiet=*/ false); if (!bool.TryParse(EnvVar, out bNeedsSign)) { int BoolAsInt; if (int.TryParse(EnvVar, out BoolAsInt)) { bNeedsSign = BoolAsInt != 0; } else { bNeedsSign = bDefaultNeedsSign; } } if (!String.IsNullOrEmpty(Params.BundleName)) { // Have to sign when a bundle name is specified bNeedsSign = true; } return bNeedsSign; } private bool IsBuiltAsFramework(ProjectParams Params, DeploymentContext SC) { UnrealTargetConfiguration Config = SC.StageTargetConfigurations[0]; string InExecutablePath = CombinePaths(Path.GetDirectoryName(Params.GetProjectExeForPlatform(TargetPlatformType).ToString()), SC.StageExecutables[0]); DirectoryReference InEngineDir = DirectoryReference.Combine(SC.LocalRoot, "Engine"); DirectoryReference InProjectDirectory = Params.RawProjectPath.Directory; bool bIsUEGame = !SC.IsCodeBasedProject; FileReference ReceiptFileName = GetTargetReceiptFileName(Config, InExecutablePath, InEngineDir, InProjectDirectory, bIsUEGame); TargetReceipt Receipt; bool bIsReadSuccessful = TargetReceipt.TryRead(ReceiptFileName, out Receipt); bool bIsBuiltAsFramework = false; if (bIsReadSuccessful) { bIsBuiltAsFramework = Receipt.HasValueForAdditionalProperty("CompileAsDll", "true"); } return bIsBuiltAsFramework; } private void StageCustomLaunchScreenStoryboard(ProjectParams Params, DeploymentContext SC) { string InterfaceSBDirectory = Path.GetDirectoryName(Params.RawProjectPath.FullName) + "/Build/IOS/Resources/Interface/"; if (Directory.Exists(InterfaceSBDirectory + "LaunchScreen.storyboardc")) { string[] StoryboardFilesToStage = Directory.GetFiles(InterfaceSBDirectory + "LaunchScreen.storyboardc", "*", SearchOption.TopDirectoryOnly); if (!DirectoryExists(SC.StageDirectory + "/LaunchScreen.storyboardc")) { DirectoryInfo createddir = Directory.CreateDirectory(SC.StageDirectory + "/LaunchScreen.storyboardc"); } foreach (string Filename in StoryboardFilesToStage) { string workingFileName = Filename; while (workingFileName.Contains("/")) { workingFileName = workingFileName.Substring(workingFileName.IndexOf('/') + 1); } workingFileName = workingFileName.Substring(workingFileName.IndexOf('/') + 1); InternalUtils.SafeCopyFile(Filename, SC.StageDirectory + "/" + workingFileName); } string[] StoryboardAssetsToStage = Directory.GetFiles(InterfaceSBDirectory + "Assets/", "*", SearchOption.TopDirectoryOnly); foreach (string Filename in StoryboardAssetsToStage) { string workingFileName = Filename; while (workingFileName.Contains("/")) { workingFileName = workingFileName.Substring(workingFileName.IndexOf('/') + 1); } workingFileName = workingFileName.Substring(workingFileName.IndexOf('/') + 1); InternalUtils.SafeCopyFile(Filename, SC.StageDirectory + "/" + workingFileName); } } else { LogWarning("Use Custom Launch Screen Storyboard is checked but not compiled storyboard could be found. Have you compiled on Mac first ? Falling back to Standard Storyboard"); StageStandardLaunchScreenStoryboard(Params, SC); } } private void StageStandardLaunchScreenStoryboard(ProjectParams Params, DeploymentContext SC) { string BuildGraphicsDirectory = Path.GetDirectoryName(Params.RawProjectPath.FullName) + "/Build/IOS/Resources/Graphics/"; if (File.Exists(BuildGraphicsDirectory + "LaunchScreenIOS.png")) { InternalUtils.SafeCopyFile(BuildGraphicsDirectory + "LaunchScreenIOS.png", SC.StageDirectory + "/LaunchScreenIOS.png"); } } private void StageLaunchScreenStoryboard(ProjectParams Params, DeploymentContext SC) { bool bCustomLaunchscreenStoryboard = false; ConfigHierarchy PlatformGameConfig; if (Params.EngineConfigs.TryGetValue(SC.StageTargetPlatform.PlatformType, out PlatformGameConfig)) { PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bCustomLaunchscreenStoryboard", out bCustomLaunchscreenStoryboard); } if (bCustomLaunchscreenStoryboard) { StageCustomLaunchScreenStoryboard(Params, SC); } else { StageStandardLaunchScreenStoryboard(Params, SC); } } public override void Package(ProjectParams Params, DeploymentContext SC, int WorkingCL) { LogInformation("Package {0}", Params.RawProjectPath); bool bIsBuiltAsFramework = IsBuiltAsFramework(Params, SC); // ensure the UnrealGame binary exists, if applicable #if !PLATFORM_MAC string ProjectGameExeFilename = Params.GetProjectExeForPlatform(TargetPlatformType).ToString(); string FullExePath = CombinePaths(Path.GetDirectoryName(ProjectGameExeFilename), SC.StageExecutables[0] + (UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac ? ".stub" : "")); if (!SC.IsCodeBasedProject && !FileExists_NoExceptions(FullExePath) && !bIsBuiltAsFramework) { LogError("Failed to find game binary " + FullExePath); throw new AutomationException(ExitCode.Error_MissingExecutable, "Stage Failed. Could not find binary {0}. You may need to build the Unreal Engine project with your target configuration and platform.", FullExePath); } #endif // PLATFORM_MAC if (SC.StageTargetConfigurations.Count != 1) { throw new AutomationException("iOS is currently only able to package one target configuration at a time, but StageTargetConfigurations contained {0} configurations", SC.StageTargetConfigurations.Count); } var TargetConfiguration = SC.StageTargetConfigurations[0]; string MobileProvision; string SigningCertificate; string TeamUUID; bool bAutomaticSigning; GetProvisioningData(Params.RawProjectPath, Params.Distribution, out MobileProvision, out SigningCertificate, out TeamUUID, out bAutomaticSigning); //@TODO: We should be able to use this code on both platforms, when the following issues are sorted: // - Raw executable is unsigned & unstripped (need to investigate adding stripping to IPP) // - IPP needs to be able to codesign a raw directory // - IPP needs to be able to take a .app directory instead of a Payload directory when doing RepackageFromStage (which would probably be renamed) // - Some discrepancy in the loading screen pngs that are getting packaged, which needs to be investigated // - Code here probably needs to be updated to write 0 byte files as 1 byte (difference with IPP, was required at one point when using Ionic.Zip to prevent issues on device, maybe not needed anymore?) if (UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) { // If we're building as a framework, then we already have everything we need in the .app // so simply package it up as an ipa if (bIsBuiltAsFramework) { PackageIPA(Params, ProjectGameExeFilename, SC); return; } // copy in all of the artwork and plist PrepForUATPackageOrDeploy(TargetConfiguration, Params.RawProjectPath, Params.ShortProjectName, Params.RawProjectPath.Directory, CombinePaths(Path.GetDirectoryName(ProjectGameExeFilename), SC.StageExecutables[0]), DirectoryReference.Combine(SC.LocalRoot, "Engine"), Params.Distribution, "", false, false, !SC.IsCodeBasedProject); // figure out where to pop in the staged files string AppDirectory = string.Format("{0}/Payload/{1}.app", Path.GetDirectoryName(ProjectGameExeFilename), Path.GetFileNameWithoutExtension(ProjectGameExeFilename)); // delete the old cookeddata InternalUtils.SafeDeleteDirectory(AppDirectory + "/cookeddata", true); InternalUtils.SafeDeleteFile(AppDirectory + "/uecommandline.txt", true); SearchOption searchMethod; if (!Params.IterativeDeploy) { searchMethod = SearchOption.AllDirectories; // copy the Staged files to the AppDirectory } else { searchMethod = SearchOption.TopDirectoryOnly; // copy just the root stage directory files } string[] StagedFiles = Directory.GetFiles(SC.StageDirectory.FullName, "*", searchMethod); foreach (string Filename in StagedFiles) { string DestFilename = Filename.Replace(SC.StageDirectory.FullName, AppDirectory); Directory.CreateDirectory(Path.GetDirectoryName(DestFilename)); InternalUtils.SafeCopyFile(Filename, DestFilename, true); } } StageLaunchScreenStoryboard(Params, SC); IOSExports.GenerateAssetCatalog(Params.RawProjectPath, new FileReference(FullExePath), new DirectoryReference(CombinePaths(Params.BaseStageDirectory, (TargetPlatformType == UnrealTargetPlatform.IOS ? "IOS" : "TVOS"))), TargetPlatformType, Log.Logger); bCreatedIPA = false; bool bNeedsIPA = false; if (Params.IterativeDeploy) { if (Params.Devices.Count != 1) { throw new AutomationException("Can only interatively deploy to a single device, but {0} were specified", Params.Devices.Count); } String NonUFSManifestPath = SC.GetNonUFSDeploymentDeltaPath(Params.DeviceNames[0]); // check to determine if we need to update the IPA if (File.Exists(NonUFSManifestPath)) { string NonUFSFiles = File.ReadAllText(NonUFSManifestPath); string[] Lines = NonUFSFiles.Split('\n'); bNeedsIPA = Lines.Length > 0 && !string.IsNullOrWhiteSpace(Lines[0]); } } if (String.IsNullOrEmpty(Params.Provision)) { Params.Provision = MobileProvision; } if (String.IsNullOrEmpty(Params.Certificate)) { Params.Certificate = SigningCertificate; } if (String.IsNullOrEmpty(Params.Team)) { Params.Team = TeamUUID; } Params.AutomaticSigning = bAutomaticSigning; // Scheme name and configuration for code signing with Xcode project string SchemeName = Params.IsCodeBasedProject ? Params.RawProjectPath.GetFileNameWithoutExtension() : "UE5"; string SchemeConfiguration = TargetConfiguration.ToString(); if (Params.Client) { SchemeConfiguration += " Client"; } WriteEntitlements(Params, SC); if (UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac) { var ProjectIPA = MakeIPAFileName(TargetConfiguration, Params, SC, Params.Distribution); var ProjectStub = Path.GetFullPath(ProjectGameExeFilename); var IPPProjectIPA = ""; if (ProjectStub.Contains("UnrealGame")) { IPPProjectIPA = Path.Combine(Path.GetDirectoryName(ProjectIPA), Path.GetFileName(ProjectIPA).Replace(Params.RawProjectPath.GetFileNameWithoutExtension(), "UnrealGame")); } // package a .ipa from the now staged directory var IPPExe = CombinePaths(CmdEnv.LocalRoot, "Engine/Binaries/DotNET/IOS/IPhonePackager.exe"); LogLog("ProjectName={0}", Params.ShortProjectName); LogLog("ProjectStub={0}", ProjectStub); LogLog("ProjectIPA={0}", ProjectIPA); LogLog("IPPProjectIPA={0}", IPPProjectIPA); LogLog("IPPExe={0}", IPPExe); bool cookonthefly = Params.CookOnTheFly || Params.SkipCookOnTheFly; // if we are incremental check to see if we need to even update the IPA if (!Params.IterativeDeploy || !File.Exists(ProjectIPA) || bNeedsIPA) { // delete the .ipa to make sure it was made DeleteFile(ProjectIPA); if (IPPProjectIPA.Length > 0) { DeleteFile(IPPProjectIPA); } bCreatedIPA = true; string IPPArguments = "RepackageFromStage \"" + (Params.IsCodeBasedProject ? Params.RawProjectPath.FullName : "Engine") + "\""; IPPArguments += " -config " + TargetConfiguration.ToString(); IPPArguments += " -schemename " + SchemeName + " -schemeconfig \"" + SchemeConfiguration + "\""; // targetname will be eg FooClient for a Client Shipping build. IPPArguments += " -targetname " + SC.StageExecutables[0].Split("-".ToCharArray())[0]; if (TargetConfiguration == UnrealTargetConfiguration.Shipping) { IPPArguments += " -compress=best"; } // Determine if we should sign bool bNeedToSign = GetCodeSignDesirability(Params); if (!String.IsNullOrEmpty(Params.BundleName)) { // Have to sign when a bundle name is specified bNeedToSign = true; IPPArguments += " -bundlename " + Params.BundleName; } if (bNeedToSign) { IPPArguments += " -sign"; if (Params.Distribution) { IPPArguments += " -distribution"; } if (Params.IsCodeBasedProject) { IPPArguments += (" -codebased"); } } if (IsBuiltAsFramework(Params, SC)) { IPPArguments += " -buildasframework"; } IPPArguments += (cookonthefly ? " -cookonthefly" : ""); string CookPlatformName = GetCookPlatform(Params.DedicatedServer, Params.Client); IPPArguments += " -stagedir \"" + CombinePaths(Params.BaseStageDirectory, CookPlatformName) + "\""; IPPArguments += " -project \"" + Params.RawProjectPath + "\""; if (Params.IterativeDeploy) { IPPArguments += " -iterate"; } if (!string.IsNullOrEmpty(Params.Provision)) { IPPArguments += " -provision \"" + Params.Provision + "\""; } if (!string.IsNullOrEmpty(Params.Certificate)) { IPPArguments += " -certificate \"" + Params.Certificate + "\""; } if (PlatformName == "TVOS") { IPPArguments += " -tvos"; } RunAndLog(CmdEnv, IPPExe, IPPArguments); if (IPPProjectIPA.Length > 0) { CopyFile(IPPProjectIPA, ProjectIPA); DeleteFile(IPPProjectIPA); } } // verify the .ipa exists if (!FileExists(ProjectIPA)) { throw new AutomationException(ExitCode.Error_FailedToCreateIPA, "PACKAGE FAILED - {0} was not created", ProjectIPA); } if (WorkingCL > 0) { // Open files for add or edit var ExtraFilesToCheckin = new List { ProjectIPA }; // check in the .ipa along with everything else UnrealBuild.AddBuildProductsToChangelist(WorkingCL, ExtraFilesToCheckin); } //@TODO: This automatically deploys after packaging, useful for testing on PC when iterating on IPP //Deploy(Params, SC); } else { // create the ipa string IPAName = CombinePaths(Path.GetDirectoryName(Params.RawProjectPath.FullName), "Binaries", PlatformName, (Params.Distribution ? "Distro_" : "") + Params.ShortProjectName + (SC.StageTargetConfigurations[0] != UnrealTargetConfiguration.Development ? ("-" + PlatformName + "-" + SC.StageTargetConfigurations[0].ToString()) : "") + ".ipa"); if (!Params.IterativeDeploy || !File.Exists(IPAName) || bNeedsIPA) { bCreatedIPA = true; // code sign the app CodeSign(Path.GetDirectoryName(ProjectGameExeFilename), Params.IsCodeBasedProject ? Params.ShortProjectName : Path.GetFileNameWithoutExtension(ProjectGameExeFilename), Params.RawProjectPath, SC.StageTargetConfigurations[0], SC.LocalRoot.FullName, Params.ShortProjectName, Path.GetDirectoryName(Params.RawProjectPath.FullName), SC.IsCodeBasedProject, Params.Distribution, Params.Provision, Params.Certificate, Params.Team, Params.AutomaticSigning, SchemeName, SchemeConfiguration); // now generate the ipa PackageIPA(Params, ProjectGameExeFilename, SC); } } PrintRunTime(); } #if true private string EnsureXcodeProjectExists(FileReference RawProjectPath, string LocalRoot, string ShortProjectName, string ProjectRoot, bool IsCodeBasedProject, out bool bWasGenerated) { // first check for the .xcodeproj bWasGenerated = false; string RawProjectDir = RawProjectPath.Directory.FullName; string XcodeProj = RawProjectPath.FullName.Replace(".uproject", "_" + PlatformName + ".xcworkspace"); if (!Directory.Exists(RawProjectDir + "/Source") && !Directory.Exists(RawProjectDir + "/Intermediate/Source")) { XcodeProj = CombinePaths(CmdEnv.LocalRoot, "Engine", Path.GetFileName(XcodeProj)); } Console.WriteLine("Project: " + XcodeProj); { // project.xcodeproj doesn't exist, so generate temp project string Arguments = "-project=\"" + RawProjectPath + "\""; Arguments += " -platforms=" + PlatformName + " -game -nointellisense -" + PlatformName + "deployonly -ignorejunk -projectfileformat=XCode -includetemptargets -automated"; // If engine is installed then UBT doesn't need to be built if (Unreal.IsEngineInstalled()) { Arguments = "-XcodeProjectFiles " + Arguments; RunUBT(CmdEnv, UnrealBuild.UnrealBuildToolDll, Arguments); } else { string Script = CombinePaths(CmdEnv.LocalRoot, "Engine/Build/BatchFiles/Mac/GenerateProjectFiles.sh"); string CWD = Directory.GetCurrentDirectory(); Directory.SetCurrentDirectory(Path.GetDirectoryName(Script)); Run(Script, Arguments, null, ERunOptions.Default); Directory.SetCurrentDirectory(CWD); } bWasGenerated = true; if (!Directory.Exists(XcodeProj)) { // something very bad happened throw new AutomationException("iOS couldn't find the appropriate Xcode Project " + XcodeProj); } } return XcodeProj; } #endif private void CodeSign(string BaseDirectory, string GameName, FileReference RawProjectPath, UnrealTargetConfiguration TargetConfig, string LocalRoot, string ProjectName, string ProjectDirectory, bool IsCode, bool Distribution = false, string Provision = null, string Certificate = null, string Team = null, bool bAutomaticSigning = false, string SchemeName = null, string SchemeConfiguration = null) { bool bUseModernXcode = false; if (OperatingSystem.IsMacOS()) { ConfigHierarchy Ini = ConfigCache.ReadHierarchy(ConfigHierarchyType.Engine, RawProjectPath.Directory, UnrealTargetPlatform.IOS); Ini.TryGetValue("XcodeConfiguration", "bUseModernXcode", out bUseModernXcode); } if (bUseModernXcode) { // we need an xcode project to continue DirectoryReference GeneratedProject = null; IOSExports.GenerateRunOnlyXcodeProject(RawProjectPath, TargetPlatformType, false, Logger, out GeneratedProject); if (GeneratedProject == null || !DirectoryReference.Exists(GeneratedProject)) { // something very bad happened throw new AutomationException("iOS couldn't find the appropriate Xcode Project for " + RawProjectPath); } int ReturnCode = IOSExports.FinalizeAppWithXcode(GeneratedProject, TargetPlatformType, SchemeName ?? GameName, SchemeConfiguration ?? TargetConfig.ToString(), Provision, Certificate, Team, bAutomaticSigning, Distribution, bUseModernXcode, Logger); if (ReturnCode != 0) { throw new AutomationException(ExitCode.Error_FailedToCodeSign, "CodeSign Failed"); } return; } // @todo make this use the new IOSExports functionality that modern mode is using, after testing it #if true // check for the proper xcodeproject bool bWasGenerated = false; string XcodeProj = EnsureXcodeProjectExists(RawProjectPath, LocalRoot, ProjectName, ProjectDirectory, IsCode, out bWasGenerated); string Arguments = "UBT_NO_POST_DEPLOY=true"; Arguments += " /usr/bin/xcrun xcodebuild build -workspace \"" + XcodeProj + "\""; Arguments += " -scheme '"; Arguments += SchemeName != null ? SchemeName : GameName; Arguments += "'"; Arguments += " -configuration \"" + (SchemeConfiguration != null ? SchemeConfiguration : TargetConfig.ToString()) + "\""; Arguments += " -destination generic/platform=" + (PlatformName == "TVOS" ? "tvOS" : "iOS"); Arguments += " -sdk " + SDKName; if (bAutomaticSigning) { Arguments += " CODE_SIGN_IDENTITY=" + (Distribution ? "\"iPhone Distribution\"" : "\"iPhone Developer\""); Arguments += " CODE_SIGN_STYLE=\"Automatic\" -allowProvisioningUpdates"; Arguments += " DEVELOPMENT_TEAM=\"" + Team + "\""; } else { if (!string.IsNullOrEmpty(Certificate)) { Arguments += " CODE_SIGN_IDENTITY=\"" + Certificate + "\""; } else { Arguments += " CODE_SIGN_IDENTITY=" + (Distribution ? "\"iPhone Distribution\"" : "\"iPhone Developer\""); } if (!string.IsNullOrEmpty(Provision)) { // read the provision to get the UUID if (File.Exists(Environment.GetEnvironmentVariable("HOME") + "/Library/MobileDevice/Provisioning Profiles/" + Provision)) { string UUID = ""; string AllText = File.ReadAllText(Environment.GetEnvironmentVariable("HOME") + "/Library/MobileDevice/Provisioning Profiles/" + Provision); int idx = AllText.IndexOf("UUID"); if (idx > 0) { idx = AllText.IndexOf("", idx); if (idx > 0) { idx += "".Length; UUID = AllText.Substring(idx, AllText.IndexOf("", idx) - idx); Arguments += " PROVISIONING_PROFILE_SPECIFIER=" + UUID; LogInformation("Extracted Provision UUID {0} from {1}", UUID, Provision); } } } } } IProcessResult Result = Run("/usr/bin/env", Arguments, null, ERunOptions.Default); if (bWasGenerated) { InternalUtils.SafeDeleteDirectory(XcodeProj, true); } if (Result.ExitCode != 0) { throw new AutomationException(ExitCode.Error_FailedToCodeSign, "CodeSign Failed"); } #endif } private bool ShouldUseMaxIPACompression(ProjectParams Params) { if (!string.IsNullOrEmpty(Params.AdditionalPackageOptions)) { string[] OptionsArray = Params.AdditionalPackageOptions.Split(' '); foreach (string Option in OptionsArray) { if (Option.Equals("-ForceMaxIPACompression", StringComparison.InvariantCultureIgnoreCase)) { return true; } } } return false; } private void PackageIPA(ProjectParams Params, string ProjectGameExeFilename, DeploymentContext SC) { string BaseDirectory = Path.GetDirectoryName(ProjectGameExeFilename); string ExeName = Params.IsCodeBasedProject ? SC.StageExecutables[0] : Path.GetFileNameWithoutExtension(ProjectGameExeFilename); string GameName = ExeName.Split("-".ToCharArray())[0]; string ProjectName = Params.ShortProjectName; UnrealTargetConfiguration TargetConfig = SC.StageTargetConfigurations[0]; // create the ipa string IPAName = MakeIPAFileName(TargetConfig, Params, SC, true); // delete the old one if (File.Exists(IPAName)) { File.Delete(IPAName); } // make the subdirectory if needed string DestSubdir = Path.GetDirectoryName(IPAName); if (!Directory.Exists(DestSubdir)) { Directory.CreateDirectory(DestSubdir); } // set up the directories string ZipWorkingDir = String.Format("Payload/{0}.app/", GameName); string ZipSourceDir = string.Format("{0}/Payload/{1}.app", BaseDirectory, GameName); // create the file using (ZipFile Zip = new ZipFile()) { // Set encoding to support unicode filenames Zip.AlternateEncodingUsage = ZipOption.Always; Zip.AlternateEncoding = Encoding.UTF8; Zip.UseZip64WhenSaving = Zip64Option.AsNecessary; // set the compression level bool bUseMaxIPACompression = ShouldUseMaxIPACompression(Params); if (Params.Distribution || bUseMaxIPACompression) { Zip.CompressionLevel = CompressionLevel.BestCompression; } // add the entire directory Zip.AddDirectory(ZipSourceDir, ZipWorkingDir); // Update permissions to be UNIX-style // Modify the file attributes of any added file to unix format foreach (ZipEntry E in Zip.Entries) { const byte FileAttributePlatform_NTFS = 0x0A; const byte FileAttributePlatform_UNIX = 0x03; const byte FileAttributePlatform_FAT = 0x00; const int UNIX_FILETYPE_NORMAL_FILE = 0x8000; //const int UNIX_FILETYPE_SOCKET = 0xC000; //const int UNIX_FILETYPE_SYMLINK = 0xA000; //const int UNIX_FILETYPE_BLOCKSPECIAL = 0x6000; const int UNIX_FILETYPE_DIRECTORY = 0x4000; //const int UNIX_FILETYPE_CHARSPECIAL = 0x2000; //const int UNIX_FILETYPE_FIFO = 0x1000; const int UNIX_EXEC = 1; const int UNIX_WRITE = 2; const int UNIX_READ = 4; int MyPermissions = UNIX_READ | UNIX_WRITE; int OtherPermissions = UNIX_READ; int PlatformEncodedBy = (E.VersionMadeBy >> 8) & 0xFF; int LowerBits = 0; // Try to preserve read-only if it was set bool bIsDirectory = E.IsDirectory; // Check to see if this bool bIsExecutable = false; if (Path.GetFileNameWithoutExtension(E.FileName).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase)) { bIsExecutable = true; } if (bIsExecutable && !bUseMaxIPACompression) { // The executable will be encrypted in the final distribution IPA and will compress very poorly, so keeping it // uncompressed gives a better indicator of IPA size for our distro builds E.CompressionLevel = CompressionLevel.None; } if ((PlatformEncodedBy == FileAttributePlatform_NTFS) || (PlatformEncodedBy == FileAttributePlatform_FAT)) { FileAttributes OldAttributes = E.Attributes; //LowerBits = ((int)E.Attributes) & 0xFFFF; if ((OldAttributes & FileAttributes.Directory) != 0) { bIsDirectory = true; } // Permissions if ((OldAttributes & FileAttributes.ReadOnly) != 0) { MyPermissions &= ~UNIX_WRITE; OtherPermissions &= ~UNIX_WRITE; } } if (bIsDirectory || bIsExecutable) { MyPermissions |= UNIX_EXEC; OtherPermissions |= UNIX_EXEC; } // Re-jigger the external file attributes to UNIX style if they're not already that way if (PlatformEncodedBy != FileAttributePlatform_UNIX) { int NewAttributes = bIsDirectory ? UNIX_FILETYPE_DIRECTORY : UNIX_FILETYPE_NORMAL_FILE; NewAttributes |= (MyPermissions << 6); NewAttributes |= (OtherPermissions << 3); NewAttributes |= (OtherPermissions << 0); // Now modify the properties E.AdjustExternalFileAttributes(FileAttributePlatform_UNIX, (NewAttributes << 16) | LowerBits); } } // Save it out Zip.Save(IPAName); } } public override void GetFilesToDeployOrStage(ProjectParams Params, DeploymentContext SC) { // if (UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac) { // copy any additional framework assets that will be needed at runtime { DirectoryReference SourcePath = DirectoryReference.Combine((SC.IsCodeBasedProject ? SC.ProjectRoot : SC.EngineRoot), "Intermediate", "IOS", "FrameworkAssets"); if (DirectoryReference.Exists(SourcePath)) { SC.StageFiles(StagedFileType.SystemNonUFS, SourcePath, StageFilesSearch.AllDirectories, StagedDirectoryReference.Root); } } // copy the plist (only if code signing, as it's protected by the code sign blob in the executable and can't be modified independently) if (GetCodeSignDesirability(Params)) { // this would be FooClient when making a client-only build string TargetName = SC.StageExecutables[0].Split("-".ToCharArray())[0]; DirectoryReference SourcePath = DirectoryReference.Combine((SC.IsCodeBasedProject ? SC.ProjectRoot : DirectoryReference.Combine(SC.LocalRoot, "Engine")), "Intermediate", PlatformName); FileReference TargetPListFile = FileReference.Combine(SourcePath, (SC.IsCodeBasedProject ? TargetName : "UnrealGame") + "-Info.plist"); // if (!File.Exists(TargetPListFile)) { // ensure the plist, entitlements, and provision files are properly copied Console.WriteLine("CookPlat {0}, this {1}", GetCookPlatform(false, false), ToString()); if (!SC.IsCodeBasedProject) { UnrealBuildTool.PlatformExports.SetRemoteIniPath(SC.ProjectRoot.FullName); } if (SC.StageTargetConfigurations.Count != 1) { throw new AutomationException("iOS is currently only able to package one target configuration at a time, but StageTargetConfigurations contained {0} configurations", SC.StageTargetConfigurations.Count); } var TargetConfiguration = SC.StageTargetConfigurations[0]; DeployGeneratePList( SC.RawProjectPath, TargetConfiguration, (SC.IsCodeBasedProject ? SC.ProjectRoot : DirectoryReference.Combine(SC.LocalRoot, "Engine")), !SC.IsCodeBasedProject, (SC.IsCodeBasedProject ? SC.StageExecutables[0] : "UnrealGame"), SC.IsCodeBasedProject ? false : Params.Client, // Code based projects will have Client in their executable name already SC.ShortProjectName, DirectoryReference.Combine(SC.LocalRoot, "Engine"), DirectoryReference.Combine((SC.IsCodeBasedProject ? SC.ProjectRoot : DirectoryReference.Combine(SC.LocalRoot, "Engine")), "Binaries", PlatformName, "Payload", (SC.IsCodeBasedProject ? SC.ShortProjectName : "UnrealGame") + ".app"), SC.StageExecutables[0]); // copy the plist to the stage dir SC.StageFile(StagedFileType.SystemNonUFS, TargetPListFile, new StagedFileReference("Info.plist")); } // copy the udebugsymbols if they exist { ConfigHierarchy PlatformGameConfig; bool bIncludeSymbols = false; if (Params.EngineConfigs.TryGetValue(SC.StageTargetPlatform.PlatformType, out PlatformGameConfig)) { PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bGenerateCrashReportSymbols", out bIncludeSymbols); } if (bIncludeSymbols) { FileReference SymbolFileName = FileReference.Combine((SC.IsCodeBasedProject ? SC.ProjectRoot : SC.EngineRoot), "Binaries", "IOS", SC.StageExecutables[0] + ".udebugsymbols"); if (FileReference.Exists(SymbolFileName)) { SC.StageFile(StagedFileType.NonUFS, SymbolFileName, new StagedFileReference((Params.ShortProjectName + ".udebugsymbols").ToLowerInvariant())); } } } } } { // Stage any *.metallib files as NonUFS. // Get the final output directory for cooked data DirectoryReference CookOutputDir; if (!String.IsNullOrEmpty(Params.CookOutputDir)) { CookOutputDir = DirectoryReference.Combine(new DirectoryReference(Params.CookOutputDir), SC.CookPlatform); } else if (Params.CookInEditor) { CookOutputDir = DirectoryReference.Combine(SC.ProjectRoot, "Saved", "EditorCooked", SC.CookPlatform); } else { CookOutputDir = DirectoryReference.Combine(SC.ProjectRoot, "Saved", "Cooked", SC.CookPlatform); } if (DirectoryReference.Exists(CookOutputDir)) { List CookedFiles = DirectoryReference.EnumerateFiles(CookOutputDir, "*.metallib", SearchOption.AllDirectories).ToList(); foreach (FileReference CookedFile in CookedFiles) { SC.StageFile(StagedFileType.NonUFS, CookedFile, new StagedFileReference(CookedFile.MakeRelativeTo(CookOutputDir))); } } } { // Stage the mute.caf file used by SoundSwitch for mute switch detection FileReference MuteCafFile = FileReference.Combine(SC.EngineRoot, "Source", "ThirdParty", "IOS", "SoundSwitch", "SoundSwitch", "SoundSwitch", "mute.caf"); if (FileReference.Exists(MuteCafFile)) { SC.StageFile(StagedFileType.SystemNonUFS, MuteCafFile, new StagedFileReference("mute.caf")); } } } protected void StageMovieFiles(DirectoryReference InputDir, DeploymentContext SC) { if (DirectoryReference.Exists(InputDir)) { foreach (FileReference InputFile in DirectoryReference.EnumerateFiles(InputDir, "*", SearchOption.AllDirectories)) { if (!InputFile.HasExtension(".uasset") && !InputFile.HasExtension(".umap")) { SC.StageFile(StagedFileType.NonUFS, InputFile); } } } } protected void StageMovieFile(DirectoryReference InputDir, string Filename, DeploymentContext SC) { if (DirectoryReference.Exists(InputDir)) { foreach (FileReference InputFile in DirectoryReference.EnumerateFiles(InputDir, "*", SearchOption.AllDirectories)) { if (!InputFile.HasExtension(".uasset") && !InputFile.HasExtension(".umap") && InputFile.GetFileNameWithoutExtension().Contains(Filename)) { SC.StageFile(StagedFileType.NonUFS, InputFile); } } } } public override void GetFilesToArchive(ProjectParams Params, DeploymentContext SC) { if (SC.StageTargetConfigurations.Count != 1) { throw new AutomationException("iOS is currently only able to package one target configuration at a time, but StageTargetConfigurations contained {0} configurations", SC.StageTargetConfigurations.Count); } var TargetConfiguration = SC.StageTargetConfigurations[0]; var ProjectIPA = MakeIPAFileName(TargetConfiguration, Params, SC, true); // verify the .ipa exists if (!FileExists(ProjectIPA)) { throw new AutomationException("ARCHIVE FAILED - {0} was not found", ProjectIPA); } ConfigHierarchy PlatformGameConfig; bool bXCArchive = false; if (Params.EngineConfigs.TryGetValue(SC.StageTargetPlatform.PlatformType, out PlatformGameConfig)) { PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bGenerateXCArchive", out bXCArchive); } if (bXCArchive && !RuntimePlatform.IsWindows) { // Always put the archive in the current user's Library/Developer/Xcode/Archives path if not on the build machine string ArchivePath = "/Users/" + Environment.UserName + "/Library/Developer/Xcode/Archives"; if (IsBuildMachine) { ArchivePath = Params.ArchiveDirectoryParam; } if (!DirectoryExists(ArchivePath)) { CreateDirectory(ArchivePath); } Console.WriteLine("Generating xc archive package in " + ArchivePath); string ArchiveName = Path.Combine(ArchivePath, Path.GetFileNameWithoutExtension(ProjectIPA) + ".xcarchive"); if (!DirectoryExists(ArchiveName)) { CreateDirectory(ArchiveName); } DeleteDirectoryContents(ArchiveName); // create the Products archive folder CreateDirectory(Path.Combine(ArchiveName, "Products", "Applications")); // copy in the application string AppName = Path.GetFileNameWithoutExtension(ProjectIPA) + ".app"; if (!File.Exists(ProjectIPA)) { Console.WriteLine("Couldn't find IPA: " + ProjectIPA); } using (ZipFile Zip = new ZipFile(ProjectIPA)) { Zip.ExtractAll(ArchivePath, ExtractExistingFileAction.OverwriteSilently); List Dirs = new List(Directory.EnumerateDirectories(Path.Combine(ArchivePath, "Payload"), "*.app")); AppName = Dirs[0].Substring(Dirs[0].LastIndexOf(UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac ? "\\" : "/") + 1); foreach (string Dir in Dirs) { if (Dir.Contains(Params.ShortProjectName + ".app")) { Console.WriteLine("Using Directory: " + Dir); AppName = Dir.Substring(Dir.LastIndexOf(UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac ? "\\" : "/") + 1); } } CopyDirectory_NoExceptions(Path.Combine(ArchivePath, "Payload", AppName), Path.Combine(ArchiveName, "Products", "Applications", AppName)); } // copy in the dSYM if found var ProjectExe = MakeIPAFileName(TargetConfiguration, Params, SC, false); string dSYMName = (SC.IsCodeBasedProject ? Path.GetFileNameWithoutExtension(ProjectExe) : "UnrealGame") + ".dSYM"; string dSYMDestName = AppName + ".dSYM"; string dSYMSrcPath = Path.Combine(SC.ProjectBinariesFolder.FullName, dSYMName); string dSYMZipSrcPath = Path.Combine(SC.ProjectBinariesFolder.FullName, dSYMName + ".zip"); if (File.Exists(dSYMZipSrcPath)) { // unzip the dsym using (ZipFile Zip = new ZipFile(dSYMZipSrcPath)) { Zip.ExtractAll(SC.ProjectBinariesFolder.FullName, ExtractExistingFileAction.OverwriteSilently); } } if (DirectoryExists(dSYMSrcPath)) { // Create the dsyms archive folder CreateDirectory(Path.Combine(ArchiveName, "dSYMs")); string dSYMDstPath = Path.Combine(ArchiveName, "dSYMs", dSYMDestName); // /Volumes/MacOSDrive1/pfEpicWorkspace/Dev-Platform/Samples/Sandbox/PlatformShowcase/Binaries/IOS/PlatformShowcase.dSYM/Contents/Resources/DWARF/PlatformShowcase CopyFile_NoExceptions(Path.Combine(dSYMSrcPath, "Contents", "Resources", "DWARF", SC.IsCodeBasedProject ? Path.GetFileNameWithoutExtension(ProjectExe) : "UnrealGame"), dSYMDstPath); } else if (File.Exists(dSYMSrcPath)) { // Create the dsyms archive folder CreateDirectory(Path.Combine(ArchiveName, "dSYMs")); string dSYMDstPath = Path.Combine(ArchiveName, "dSYMs", dSYMDestName); CopyFile_NoExceptions(dSYMSrcPath, dSYMDstPath); } // copy in the bitcode symbol maps if found string[] bcmapfiles = Directory.GetFiles(SC.ProjectBinariesFolder.FullName, "*.bcsymbolmap"); if (bcmapfiles.Length > 0) { // Create the dsyms archive folder CreateDirectory(Path.Combine(ArchiveName, "BCSymbolMaps")); foreach (string symbolSrcFilePath in bcmapfiles) { string symbolLeafFileName = Path.GetFileName(symbolSrcFilePath); string bcDstFilePath = Path.Combine(ArchiveName, "BCSymbolMaps", symbolLeafFileName); CopyFile_NoExceptions(symbolSrcFilePath, bcDstFilePath); } } // get the settings from the app plist file string AppPlist = Path.Combine(ArchiveName, "Products", "Applications", AppName, "Info.plist"); string OldPListData = File.Exists(AppPlist) ? File.ReadAllText(AppPlist) : ""; string BundleIdentifier = ""; string BundleShortVersion = ""; string BundleVersion = ""; if (!string.IsNullOrEmpty(OldPListData)) { // bundle identifier int index = OldPListData.IndexOf("CFBundleIdentifier"); index = OldPListData.IndexOf("", index) + 8; int length = OldPListData.IndexOf("", index) - index; BundleIdentifier = OldPListData.Substring(index, length); // short version index = OldPListData.IndexOf("CFBundleShortVersionString"); index = OldPListData.IndexOf("", index) + 8; length = OldPListData.IndexOf("", index) - index; BundleShortVersion = OldPListData.Substring(index, length); // bundle version index = OldPListData.IndexOf("CFBundleVersion"); index = OldPListData.IndexOf("", index) + 8; length = OldPListData.IndexOf("", index) - index; BundleVersion = OldPListData.Substring(index, length); } else { Console.WriteLine("Could not load Info.plist"); } // date we made this const string Iso8601DateTimeFormat = "yyyy-MM-ddTHH:mm:ssZ"; string TimeStamp = DateTime.UtcNow.ToString(Iso8601DateTimeFormat); // create the archive plist StringBuilder Text = new StringBuilder(); Text.AppendLine(""); Text.AppendLine(""); Text.AppendLine(""); Text.AppendLine(""); Text.AppendLine("\tApplicationProperties"); Text.AppendLine("\t"); Text.AppendLine("\t\tApplicationPath"); Text.AppendLine("\t\tApplications/" + AppName + ""); Text.AppendLine("\t\tCFBundleIdentifier"); Text.AppendLine(string.Format("\t\t{0}", BundleIdentifier)); Text.AppendLine("\t\tCFBundleShortVersionString"); Text.AppendLine(string.Format("\t\t{0}", BundleShortVersion)); Text.AppendLine("\t\tCFBundleVersion"); Text.AppendLine(string.Format("\t\t{0}", BundleVersion)); Text.AppendLine("\t\tSigningIdentity"); Text.AppendLine(string.Format("\t\t{0}", Params.Certificate)); Text.AppendLine("\t"); Text.AppendLine("\tArchiveVersion"); Text.AppendLine("\t2"); Text.AppendLine("\tCreationDate"); Text.AppendLine(string.Format("\t{0}", TimeStamp)); Text.AppendLine("\tDefaultToolchainInfo"); Text.AppendLine("\t"); Text.AppendLine("\t\tDisplayName"); Text.AppendLine("\t\tXcode 7.3 Default"); Text.AppendLine("\t\tIdentifier"); Text.AppendLine("\t\tcom.apple.dt.toolchain.XcodeDefault"); Text.AppendLine("\t"); Text.AppendLine("\tName"); Text.AppendLine(string.Format("\t{0}", SC.ShortProjectName)); Text.AppendLine("\tSchemeName"); Text.AppendLine(string.Format("\t{0}", SC.ShortProjectName)); Text.AppendLine(""); Text.AppendLine(""); File.WriteAllText(Path.Combine(ArchiveName, "Info.plist"), Text.ToString()); } else if (bXCArchive && RuntimePlatform.IsWindows) { LogWarning("Can not produce an XCArchive on windows"); } SC.ArchiveFiles(Path.GetDirectoryName(ProjectIPA), Path.GetFileName(ProjectIPA)); } public override bool RetrieveDeployedManifests(ProjectParams Params, DeploymentContext SC, string DeviceName, out List UFSManifests, out List NonUFSManifests) { if (Params.Devices.Count != 1) { throw new AutomationException("Can only retrieve deployed manifests from a single device, but {0} were specified", Params.Devices.Count); } bool Result = true; UFSManifests = new List(); NonUFSManifests = new List(); try { var TargetConfiguration = SC.StageTargetConfigurations[0]; string BundleIdentifier = ""; if (File.Exists(Params.BaseStageDirectory + "/" + PlatformName + "/Info.plist")) { string Contents = File.ReadAllText(SC.StageDirectory + "/Info.plist"); int Pos = Contents.IndexOf("CFBundleIdentifier"); Pos = Contents.IndexOf("", Pos) + 8; int EndPos = Contents.IndexOf("", Pos); BundleIdentifier = Contents.Substring(Pos, EndPos - Pos); } string IdeviceInstallerArgs = "--list-apps -u " + Params.DeviceNames[0]; IdeviceInstallerArgs = GetLibimobileDeviceNetworkedArgument(IdeviceInstallerArgs, Params.DeviceNames[0]); var DeviceInstaller = GetPathToLibiMobileDeviceTool("ideviceinstaller"); LogInformation("Checking if bundle {0} is installed", BundleIdentifier); string Output = CommandUtils.RunAndLog(DeviceInstaller, IdeviceInstallerArgs); bool bBundleIsInstalled = Output.Contains(string.Format("CFBundleIdentifier -> {0}{1}", BundleIdentifier, Environment.NewLine)); int ExitCode = 0; if (bBundleIsInstalled) { LogInformation("Bundle {0} found, retrieving deployed manifests...", BundleIdentifier); var DeviceFS = GetPathToLibiMobileDeviceTool("idevicefs"); string AllCommandsToPush = " push " + CombinePaths(Params.BaseStageDirectory, PlatformName, SC.GetUFSDeployedManifestFileName(null)) + "\n" + " push " + CombinePaths(Params.BaseStageDirectory, PlatformName, SC.GetNonUFSDeployedManifestFileName(null)); System.IO.File.WriteAllText(Directory.GetCurrentDirectory() + "\\CommandsToPush.txt", AllCommandsToPush); string IdeviceFSArgs = "-b " + "\"" + BundleIdentifier + " -x " + Directory.GetCurrentDirectory() + "\\CommandsToPush.txt -u " + "\"" + Params.DeviceNames[0]; IdeviceFSArgs = GetLibimobileDeviceNetworkedArgument(IdeviceFSArgs, Params.DeviceNames[0]); Utils.RunLocalProcessAndReturnStdOut(DeviceFS, IdeviceFSArgs, Log.Logger, out ExitCode); if (ExitCode != 0) { throw new AutomationException("Failed to deploy manifest to mobile device."); } string[] ManifestFiles = Directory.GetFiles(CombinePaths(Params.BaseStageDirectory, PlatformName), "*_Manifest_UFS*.txt"); UFSManifests.AddRange(ManifestFiles); ManifestFiles = Directory.GetFiles(CombinePaths(Params.BaseStageDirectory, PlatformName), "*_Manifest_NonUFS*.txt"); NonUFSManifests.AddRange(ManifestFiles); } else { LogInformation("Bundle {0} not found, skipping retrieving deployed manifests", BundleIdentifier); } } catch (System.Exception) { // delete any files that did get copied string[] Manifests = Directory.GetFiles(CombinePaths(Params.BaseStageDirectory, PlatformName), "*_Manifest_*.txt"); foreach (string Manifest in Manifests) { File.Delete(Manifest); } Result = false; } return Result; } private string GetPathToLibiMobileDeviceTool(string LibimobileExec) { string ExecWithPath = ""; if (UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64) { ExecWithPath = CombinePaths(CmdEnv.LocalRoot, "Engine/Extras/ThirdPartyNotUE/libimobiledevice/x64/" + LibimobileExec + ".exe"); } else if (UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) { ExecWithPath = CombinePaths(CmdEnv.LocalRoot, "Engine/Extras/ThirdPartyNotUE/libimobiledevice/Mac/" + LibimobileExec); } if (!File.Exists(ExecWithPath) || ExecWithPath == "") { throw new AutomationException("Failed to locate LibiMobileDevice executable."); } return ExecWithPath; } private string GetLibimobileDeviceNetworkedArgument(string EntryArguments, string UDID) { DeviceInfo[] CachedDevices = GetDevices(); if (CachedDevices.Where(CachedDevice => CachedDevice.Id == UDID && CachedDevice.PlatformValues["Connection"] == "Network").Count() > 0) { return EntryArguments + " -n"; } return EntryArguments; } private void DeployManifestContent(string BaseFolder, DeploymentContext SC, ProjectParams Params, ref string Files, ref string BundleIdentifier) { var DeviceFS = GetPathToLibiMobileDeviceTool("idevicefs"); string[] FileList = Files.Split('\n'); string AllCommandsToPush = ""; foreach (string Filename in FileList) { if (!string.IsNullOrEmpty(Filename) && !string.IsNullOrWhiteSpace(Filename)) { string Trimmed = Filename.Trim(); string SourceFilename = BaseFolder + "\\" + Trimmed; SourceFilename = SourceFilename.Replace('/', '\\'); string DestFilename = "/Library/Caches/" + Trimmed.Replace("cookeddata/", ""); DestFilename = DestFilename.Replace('\\', '/'); DestFilename = "\"" + DestFilename + "\""; SourceFilename = SourceFilename.Replace('\\', Path.DirectorySeparatorChar); string CommandToPush = "push -p \"" + SourceFilename + "\" " + DestFilename + "\n"; AllCommandsToPush += CommandToPush; } } System.IO.File.WriteAllText(Directory.GetCurrentDirectory() + "\\CommandsToPush.txt", AllCommandsToPush); int ExitCode = 0; string IdeviceFSArgs = "-u " + Params.DeviceNames[0] + " -b " + BundleIdentifier + " -x " + "\"" + Directory.GetCurrentDirectory() + "\\CommandsToPush.txt" + "\""; IdeviceFSArgs = GetLibimobileDeviceNetworkedArgument(IdeviceFSArgs, Params.DeviceNames[0]); using (Process IDeviceFSProcess = new Process()) { DataReceivedEventHandler StdOutHandler = (E, Args) => { if (Args.Data != null) { Log.TraceInformation("{0}", Args.Data); } }; DataReceivedEventHandler StdErrHandler = (E, Args) => { if (Args.Data != null) { Log.TraceError("{0}", Args.Data); } }; IDeviceFSProcess.StartInfo.FileName = DeviceFS; IDeviceFSProcess.StartInfo.Arguments = IdeviceFSArgs; IDeviceFSProcess.OutputDataReceived += StdOutHandler; IDeviceFSProcess.ErrorDataReceived += StdErrHandler; ExitCode = Utils.RunLocalProcess(IDeviceFSProcess); if (ExitCode != 0) { throw new AutomationException("Failed to push content to mobile device."); } } File.Delete(Directory.GetCurrentDirectory() + "\\CommandsToPush.txt"); } public override void Deploy(ProjectParams Params, DeploymentContext SC) { if (Params.Devices.Count != 1) { throw new AutomationException("Can only deploy to a single specified device, but {0} were specified", Params.Devices.Count); } if (SC.StageTargetConfigurations.Count != 1) { throw new AutomationException("iOS is currently only able to package one target configuration at a time, but StageTargetConfigurations contained {0} configurations", SC.StageTargetConfigurations.Count); } if (Params.Distribution) { throw new AutomationException("iOS cannot deploy a package made for distribution."); } var TargetConfiguration = SC.StageTargetConfigurations[0]; var ProjectIPA = MakeIPAFileName(TargetConfiguration, Params, SC, true); var StagedIPA = SC.StageDirectory + "\\" + Path.GetFileName(ProjectIPA); // verify the .ipa exists if (!FileExists(StagedIPA)) { StagedIPA = ProjectIPA; if (!FileExists(StagedIPA)) { throw new AutomationException("DEPLOY FAILED - {0} was not found", ProjectIPA); } } // if iterative deploy, determine the file delta string BundleIdentifier = ""; bool bNeedsIPA = true; if (Params.IterativeDeploy) { if (File.Exists(Params.BaseStageDirectory + "/" + PlatformName + "/Info.plist")) { string Contents = File.ReadAllText(SC.StageDirectory + "/Info.plist"); int Pos = Contents.IndexOf("CFBundleIdentifier"); Pos = Contents.IndexOf("", Pos) + 8; int EndPos = Contents.IndexOf("", Pos); BundleIdentifier = Contents.Substring(Pos, EndPos - Pos); } // check to determine if we need to update the IPA String NonUFSManifestPath = SC.GetNonUFSDeploymentDeltaPath(Params.DeviceNames[0]); if (File.Exists(NonUFSManifestPath)) { string NonUFSFiles = File.ReadAllText(NonUFSManifestPath); string[] Lines = NonUFSFiles.Split('\n'); bNeedsIPA = Lines.Length > 0 && !string.IsNullOrWhiteSpace(Lines[0]); } } // Add a commandline for this deploy, if the config allows it. string AdditionalCommandline = (Params.FileServer || Params.CookOnTheFly) ? "" : (" -additionalcommandline " + "\"" + Params.RunCommandline + "\""); // deploy the .ipa var DeviceInstaller = GetPathToLibiMobileDeviceTool("ideviceinstaller"); string LibimobileDeviceArguments = "-u " + Params.DeviceNames[0] + " -i " + "\"" + Path.GetFullPath(StagedIPA) + "\""; LibimobileDeviceArguments = GetLibimobileDeviceNetworkedArgument(LibimobileDeviceArguments, Params.DeviceNames[0]); // check for it in the stage directory string CurrentDir = Directory.GetCurrentDirectory(); Directory.SetCurrentDirectory(CombinePaths(CmdEnv.LocalRoot, "Engine/Binaries/DotNET/IOS/")); if (!Params.IterativeDeploy || bCreatedIPA || bNeedsIPA) { RunAndLog(CmdEnv, DeviceInstaller, LibimobileDeviceArguments); } // deploy the assets if (Params.IterativeDeploy) { string BaseFolder = Path.GetDirectoryName(SC.GetUFSDeploymentDeltaPath(Params.DeviceNames[0])); string FilesString = File.ReadAllText(SC.GetUFSDeploymentDeltaPath(Params.DeviceNames[0])); DeployManifestContent(BaseFolder, SC, Params, ref FilesString, ref BundleIdentifier); if (bNeedsIPA) { BaseFolder = Path.GetDirectoryName(SC.GetNonUFSDeploymentDeltaPath(Params.DeviceNames[0])); FilesString = File.ReadAllText(SC.GetNonUFSDeploymentDeltaPath(Params.DeviceNames[0])); DeployManifestContent(BaseFolder, SC, Params, ref FilesString, ref BundleIdentifier); } Directory.SetCurrentDirectory(CurrentDir); PrintRunTime(); } } public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly) { return bIsClientOnly ? "IOSClient" : "IOS"; } public override bool DeployLowerCaseFilenames(StagedFileType FileType) { // we shouldn't modify the case on files like Info.plist or the icons return true; } public override string LocalPathToTargetPath(string LocalPath, string LocalRoot) { return LocalPath.Replace("\\", "/").Replace(LocalRoot, "../../.."); } public override bool IsSupported { get { return true; } } public override bool LaunchViaUFE { get { return UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac; } } public override bool UseAbsLog { get { return !LaunchViaUFE; } } public override bool RemapFileType(StagedFileType FileType) { return ( FileType == StagedFileType.UFS || FileType == StagedFileType.NonUFS || FileType == StagedFileType.DebugNonUFS); } public override StagedFileReference Remap(StagedFileReference Dest) { return new StagedFileReference("cookeddata/" + Dest.Name); } public override List GetDebugFileExtensions() { return new List { ".dsym", ".udebugsymbols" }; } public override IProcessResult RunClient(ERunOptions ClientRunFlags, string ClientApp, string ClientCmdLine, ProjectParams Params) { if (UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac || UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Win64) { if (Params.Devices.Count != 1) { throw new AutomationException("Can only run on a single specified device, but {0} were specified", Params.Devices.Count); } string BundleIdentifier = ""; if (File.Exists(Params.BaseStageDirectory + "/" + PlatformName + "/Info.plist")) { string Contents = File.ReadAllText(Params.BaseStageDirectory + "/" + PlatformName + "/Info.plist"); int Pos = Contents.IndexOf("CFBundleIdentifier"); Pos = Contents.IndexOf("", Pos) + 8; int EndPos = Contents.IndexOf("", Pos); BundleIdentifier = Contents.Substring(Pos, EndPos - Pos); } string Program = GetPathToLibiMobileDeviceTool("idevicedebug"); ; string Arguments = " -u '" + Params.DeviceNames[0] + "'"; Arguments = GetLibimobileDeviceNetworkedArgument(Arguments, Params.DeviceNames[0]); Arguments += " run '" + BundleIdentifier + "'"; IProcessResult ClientProcess = Run(Program, Arguments, null, ClientRunFlags | ERunOptions.NoWaitForExit); if (ClientProcess.ExitCode == -1) { Console.WriteLine("The application {0} has been installed on the device {1} but it cannot be launched automatically because the device does not contain the required developer software. You can launch {0} the manually by clicking its icon on the device.", BundleIdentifier, Params.DeviceNames[0]); Console.WriteLine("To install the developer software tools, connect it to a Mac running Xcode, open the Devices and Simulators window and wait for the tools to be installed."); IProcessResult Result = new ProcessResult("DummyApp", null, false); Result.ExitCode = 0; return Result; } return new IOSClientProcess(ClientProcess, Params.DeviceNames[0]); } else { IProcessResult Result = new ProcessResult("DummyApp", null, false); Result.ExitCode = 0; return Result; } } public override void PostRunClient(IProcessResult Result, ProjectParams Params) { if (UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) { string LaunchTracePath = Params.BaseStageDirectory + "/" + PlatformName + "/launch.trace"; Console.WriteLine("Deleting " + LaunchTracePath); if (Directory.Exists(LaunchTracePath)) { Directory.Delete(LaunchTracePath, true); } switch (Result.ExitCode) { case 253: throw new AutomationException(ExitCode.Error_DeviceNotSetupForDevelopment, "Launch Failure"); case 255: throw new AutomationException(ExitCode.Error_DeviceOSNewerThanSDK, "Launch Failure"); } } } private static int GetChunkCount(ProjectParams Params, DeploymentContext SC) { var ChunkListFilename = GetChunkPakManifestListFilename(Params, SC); var ChunkArray = ReadAllLines(ChunkListFilename); return ChunkArray.Length; } private static string GetChunkPakManifestListFilename(ProjectParams Params, DeploymentContext SC) { return CombinePaths(GetTmpPackagingPath(Params, SC), "pakchunklist.txt"); } private static string GetTmpPackagingPath(ProjectParams Params, DeploymentContext SC) { return CombinePaths(Path.GetDirectoryName(Params.RawProjectPath.FullName), "Saved", "TmpPackaging", SC.StageTargetPlatform.GetCookPlatform(SC.DedicatedServer, false)); } private static StringBuilder AppendKeyValue(StringBuilder Text, string Key, object Value, int Level) { // create indent level string Indent = ""; for (int i = 0; i < Level; ++i) { Indent += "\t"; } // output key if we have one if (Key != null) { Text.AppendLine(Indent + "" + Key + ""); } // output value if (Value is Array) { Text.AppendLine(Indent + ""); Array ValArray = Value as Array; foreach (var Item in ValArray) { AppendKeyValue(Text, null, Item, Level + 1); } Text.AppendLine(Indent + ""); } else if (Value is Dictionary) { Text.AppendLine(Indent + ""); Dictionary ValDict = Value as Dictionary; foreach (var Item in ValDict) { AppendKeyValue(Text, Item.Key, Item.Value, Level + 1); } Text.AppendLine(Indent + ""); } else if (Value is string) { Text.AppendLine(Indent + "" + Value + ""); } else if (Value is bool) { if ((bool)Value == true) { Text.AppendLine(Indent + ""); } else { Text.AppendLine(Indent + ""); } } else { Console.WriteLine("PLIST: Unknown array item type"); } return Text; } private static void GeneratePlist(Dictionary KeyValues, string PlistFile) { // generate the plist file StringBuilder Text = new StringBuilder(); // boiler plate top Text.AppendLine(""); Text.AppendLine(""); Text.AppendLine(""); Text.AppendLine(""); foreach (var KeyValue in KeyValues) { AppendKeyValue(Text, KeyValue.Key, KeyValue.Value, 1); } Text.AppendLine(""); Text.AppendLine(""); // write the file out if (!Directory.Exists(Path.GetDirectoryName(PlistFile))) { Directory.CreateDirectory(Path.GetDirectoryName(PlistFile)); } File.WriteAllText(PlistFile, Text.ToString()); } private static void GenerateAssetPlist(string BundleIdentifier, string[] Tags, string AssetDir) { Dictionary KeyValues = new Dictionary(); KeyValues.Add("CFBundleIdentifier", BundleIdentifier); KeyValues.Add("Tags", Tags); GeneratePlist(KeyValues, CombinePaths(AssetDir, "Info.plist")); } private static void GenerateAssetPackManifestPlist(KeyValuePair[] ChunkData, string AssetDir) { Dictionary[] Resources = new Dictionary[ChunkData.Length]; for (int i = 0; i < ChunkData.Length; ++i) { Dictionary Data = new Dictionary(); Data.Add("URL", CombinePaths("OnDemandResources", ChunkData[i].Value)); Data.Add("bundleKey", ChunkData[i].Key); Data.Add("isStreamable", false); Resources[i] = Data; } Dictionary KeyValues = new Dictionary(); KeyValues.Add("resources", Resources); GeneratePlist(KeyValues, CombinePaths(AssetDir, "AssetPackManifest.plist")); } private static void GenerateOnDemandResourcesPlist(KeyValuePair[] ChunkData, string AssetDir) { Dictionary RequestTags = new Dictionary(); Dictionary AssetPacks = new Dictionary(); Dictionary Requests = new Dictionary(); for (int i = 0; i < ChunkData.Length; ++i) { string ChunkName = "Chunk" + (i + 1).ToString(); RequestTags.Add(ChunkName, new string[] { ChunkData[i].Key }); AssetPacks.Add(ChunkData[i].Key, new string[] { ("pak" + ChunkName + "-ios.pak").ToLowerInvariant() }); Dictionary Packs = new Dictionary(); Packs.Add("NSAssetPacks", new string[] { ChunkData[i].Key }); Requests.Add(ChunkName, Packs); } Dictionary KeyValues = new Dictionary(); KeyValues.Add("NSBundleRequestTags", RequestTags); KeyValues.Add("NSBundleResourceRequestAssetPacks", AssetPacks); KeyValues.Add("NSBundleResourceRequestTags", Requests); GeneratePlist(KeyValues, CombinePaths(AssetDir, "OnDemandResources.plist")); } public override void PostStagingFileCopy(ProjectParams Params, DeploymentContext SC) { /* if (Params.CreateChunkInstall) { // get the bundle identifier string BundleIdentifier = ""; if (File.Exists(Params.BaseStageDirectory + "/" + PlatformName + "/Info.plist")) { string Contents = File.ReadAllText(SC.StageDirectory + "/Info.plist"); int Pos = Contents.IndexOf("CFBundleIdentifier"); Pos = Contents.IndexOf("", Pos) + 8; int EndPos = Contents.IndexOf("", Pos); BundleIdentifier = Contents.Substring(Pos, EndPos - Pos); } // generate the ODR resources // create the ODR directory string DestSubdir = SC.StageDirectory + "/OnDemandResources"; if (!Directory.Exists(DestSubdir)) { Directory.CreateDirectory(DestSubdir); } // read the chunk list and generate the data var ChunkCount = GetChunkCount(Params, SC); var ChunkData = new KeyValuePair[ChunkCount - 1]; for (int i = 1; i < ChunkCount; ++i) { // chunk name string ChunkName = "Chunk" + i.ToString (); // asset name string AssetPack = BundleIdentifier + ".Chunk" + i.ToString () + ".assetpack"; // bundle key byte[] bytes = new byte[ChunkName.Length * sizeof(char)]; System.Buffer.BlockCopy(ChunkName.ToCharArray(), 0, bytes, 0, bytes.Length); string BundleKey = BundleIdentifier + ".asset-pack-" + BitConverter.ToString(System.Security.Cryptography.MD5.Create().ComputeHash(bytes)).Replace("-", string.Empty); // add to chunk data ChunkData[i-1] = new KeyValuePair(BundleKey, AssetPack); // create the sub directory string AssetDir = CombinePaths (DestSubdir, AssetPack); if (!Directory.Exists(AssetDir)) { Directory.CreateDirectory(AssetDir); } // generate the Info.plist for each ODR bundle (each chunk for install past 0) GenerateAssetPlist (BundleKey, new string[] { ChunkName }, AssetDir); // copy the files to the OnDemandResources directory string PakName = "pakchunk" + i.ToString (); string FileName = PakName + "-" + PlatformName.ToLower() + ".pak"; string P4Change = "UnknownCL"; string P4Branch = "UnknownBranch"; if (CommandUtils.P4Enabled) { P4Change = CommandUtils.P4Env.ChangelistString; P4Branch = CommandUtils.P4Env.BuildRootEscaped; } string ChunkInstallBasePath = CombinePaths(SC.ProjectRoot.FullName, "ChunkInstall", SC.FinalCookPlatform); string RawDataPath = CombinePaths(ChunkInstallBasePath, P4Branch + "-CL-" + P4Change, PakName); string RawDataPakPath = CombinePaths(RawDataPath, PakName + "-" + SC.FinalCookPlatform + ".pak"); string DestFile = CombinePaths (AssetDir, FileName); CopyFile (RawDataPakPath, DestFile); } // generate the AssetPackManifest.plist GenerateAssetPackManifestPlist (ChunkData, SC.StageDirectory.FullName); // generate the OnDemandResources.plist GenerateOnDemandResourcesPlist (ChunkData, SC.StageDirectory.FullName); }*/ } public override bool RequiresPackageToDeploy { get { return true; } } public override HashSet GetFilesForCRCCheck() { HashSet FileList = base.GetFilesForCRCCheck(); FileList.Add(new StagedFileReference("Info.plist")); return FileList; } public override bool SupportsMultiDeviceDeploy { get { return true; } } public override void StripSymbols(FileReference SourceFile, FileReference TargetFile) { IOSExports.StripSymbols(PlatformType, SourceFile, TargetFile, Log.Logger); } private void WriteEntitlements(ProjectParams Params, DeploymentContext SC) { // game name string AppName = SC.IsCodeBasedProject ? SC.StageExecutables[0].Split("-".ToCharArray())[0] : "UnrealGame"; // mobile provisioning file DirectoryReference MobileProvisionDir; if (BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac) { MobileProvisionDir = DirectoryReference.Combine(new DirectoryReference(Environment.GetEnvironmentVariable("HOME")), "Library", "MobileDevice", "Provisioning Profiles"); } else { MobileProvisionDir = DirectoryReference.Combine(DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.LocalApplicationData), "Apple Computer", "MobileDevice", "Provisioning Profiles"); } FileReference MobileProvisionFile = null; if(MobileProvisionDir != null && Params.Provision != null) { MobileProvisionFile = FileReference.Combine(MobileProvisionDir, Params.Provision); } // distribution build bool bForDistribution = Params.Distribution; // intermediate directory string IntermediateDir = SC.ProjectRoot + "/Intermediate/" + (TargetPlatformType == UnrealTargetPlatform.IOS ? "IOS" : "TVOS"); // entitlements file name string OutputFilename = Path.Combine(IntermediateDir, AppName + ".entitlements"); // ios configuration from the ini file ConfigHierarchy PlatformGameConfig; if (Params.EngineConfigs.TryGetValue(SC.StageTargetPlatform.PlatformType, out PlatformGameConfig)) { IOSExports.WriteEntitlements(TargetPlatformType, PlatformGameConfig, AppName, MobileProvisionFile, bForDistribution, IntermediateDir); } } public override DirectoryReference GetProjectRootForStage(DirectoryReference RuntimeRoot, StagedDirectoryReference RelativeProjectRootForStage) { return DirectoryReference.Combine(RuntimeRoot, "cookeddata/" + RelativeProjectRootForStage.Name); } public override void PrepareForDebugging(string SourcePackage, string ProjectFilePath, string ClientPlatform) { if (HostPlatform.Current.HostEditorPlatform != UnrealTargetPlatform.Mac) { LogInformation("Wrangling data for debug for an iOS/tvOS app for XCode is a Mac only feature. Aborting command."); return; } int StartPos = ProjectFilePath.LastIndexOf("/"); int StringLength = ProjectFilePath.Length - 10; // 9 for .uproject, 1 for the / string PackageName = ProjectFilePath.Substring(StartPos + 1, StringLength - StartPos); if (string.IsNullOrEmpty(SourcePackage)) { SourcePackage = ProjectFilePath; SourcePackage = SourcePackage.Substring(0, SourcePackage.LastIndexOf('/')); SourcePackage = SourcePackage + "/Build/" + ClientPlatform + '/' + PackageName + ".ipa"; } string ZipFile = SourcePackage.Replace(".ipa", ".zip"); string PayloadPath = SourcePackage; PayloadPath = PayloadPath.Substring(0, PayloadPath.LastIndexOf('/')); PayloadPath += "/Payload/"; string CookedDataDirectory = PayloadPath + PackageName + ".app/cookeddata/"; LogInformation("ClientPlatform : {0}", ClientPlatform); LogInformation("ProjectFilePath : {0}", ProjectFilePath); LogInformation("Source : {0}", SourcePackage); LogInformation("ZipFile {0}", ZipFile); LogInformation("PackageName {0}", PackageName); LogInformation("PayloadPath {0}", PayloadPath); if (File.Exists(ZipFile)) { LogInformation("Deleting previously present ZIP file created from IPA"); File.Delete(ZipFile); } File.Copy(SourcePackage, ZipFile); UnzipPackage(ZipFile); string ProjectPath = ProjectFilePath; int Index = ProjectFilePath.IndexOf("?"); if (Index >= 0) { ProjectPath = ProjectPath.Substring(0, Index); } if (Directory.Exists(ProjectPath + "/Binaries/" + ClientPlatform + '/' + "Payload/" + PackageName + ".app")) { CopyDirectory_NoExceptions(CookedDataDirectory, ProjectPath + "/Binaries/" + ClientPlatform + "/Payload/" + PackageName + ".app/cookeddata/", true); } else { string ProjectRoot = SourcePackage; ProjectRoot = ProjectRoot.Substring(0, ProjectRoot.LastIndexOf('/')); CopyFile(SourcePackage, ProjectRoot + "/Binaries/" + ClientPlatform + "/Payload/" + PackageName + ".ipa", true); } //cleanup LogInformation("Deleting temp files ..."); File.Delete(ZipFile); LogInformation("{0} deleted", ZipFile); Directory.Delete(PayloadPath, true); LogInformation("{0} deleted", PayloadPath); } public void UnzipPackage(string PackageToUnzip) { string UnzipPath = PackageToUnzip; UnzipPath = UnzipPath.Substring(0, UnzipPath.LastIndexOf('/')); LogInformation("Unzipping to {0}", UnzipPath); using (Ionic.Zip.ZipFile Zip = new Ionic.Zip.ZipFile(PackageToUnzip)) { foreach (Ionic.Zip.ZipEntry Entry in Zip.Entries.Where(x => !x.IsDirectory)) { string OutputFileName = Path.Combine(UnzipPath, Entry.FileName); Directory.CreateDirectory(Path.GetDirectoryName(OutputFileName)); using (FileStream OutputStream = new FileStream(OutputFileName, FileMode.Create, FileAccess.Write)) { Entry.Extract(OutputStream); } LogInformation("Extracted {0}", OutputFileName); } } } }