Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/IOS/IOSPlatform.Automation.cs
Jack Porter 2082f7ec9b Copying //UE4/Dev-Mobile to //UE4/Dev-Main (Source: //UE4/Dev-Mobile @ 3056055)
#lockdown Nick.Penwarden
#rb None

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3011102 on 2016/06/13 by Steve.Cano

	After taking a screenshot using glReadPixels, transfer the data to the target buffer from bottom row up to fix the "upside-down" render that OpenGL does. Confirmed with QA (owen.stupka_volt) that this does not appear to be happening on iOS (non-metal devices, inclusion of iOS in write-up was a mistake), verified on an ipod touch 5. Also confirmed that this does not happen on html5, and that Mobile HDR flag does not make a difference in function.

	#jira UE-26421
	#ue4
	#android

Change 3015801 on 2016/06/16 by Dmitriy.Dyomin

	Probbably fix for UE-30878, was not able to repro an actual crash(FFoliageInstanceBaseCache::AddInstanceBaseId). Added even more logging in case fix does not work.
	#jira  UE-30878

Change 3015903 on 2016/06/16 by Dmitriy.Dyomin

	Fixed: Levels window has Refresh/UI issues when World Composition is active
	#jira UE-26160

Change 3018352 on 2016/06/17 by Chris.Babcock

	Handle Android media prepare failure (URL without internet for example)
	#jira UE-32029
	#ue4
	#android

Change 3026387 on 2016/06/24 by Jack.Porter

	Remove FFuncTestManager warning about PIE when running on a standalone game binary

Change 3026398 on 2016/06/24 by Jack.Porter

	Prevent FSocketBSD::Recv returning false on SE_EWOULDBLOCK

Change 3027553 on 2016/06/25 by Niklas.Smedberg

	OpenGL: Made some block size calculation work for arbitrary block sizes (e.g. not pow-of-two).

Change 3027554 on 2016/06/25 by Niklas.Smedberg

	Metal: copyFromTexture now gets block-aligned size parameter (e.g. used for texture streaming)

Change 3028061 on 2016/06/26 by Jack.Porter

	Fixed a problem where newly discovered instances were not added to an existing session in the Session Browser.
	Fixed a problem where selecting an instance in a session with multiple instances didn't deselect the previously selected instance correctly.

Change 3029220 on 2016/06/27 by Steve.Cano

	Change Android Tilt values to use GetRotationMatrix/GetOrientation logic, same as java-side android would use, and adjust slightly to match as closely as possible to iOS values for tilt. There is drift and some differences in the "Y" value but the same sort of inconsistencies are also seen on iOS.

	#jira UE-6135
	#ue4
	#android

Change 3030420 on 2016/06/28 by Jack.Porter

	Fix crash with RenderOutputValidation when running with cooked content

Change 3030426 on 2016/06/28 by Jack.Porter

	Fix to CL 3026398 - make FSocketBSD(IPv6)::Recv(From) return false when recv returns 0.
	A return value of 0 indicates the connection was shutdown in an orderly manner.

Change 3030973 on 2016/06/28 by Steve.Cano

	Added a landscape downloader background along with the options to change it from within Android settings

	#ue4
	#android
	#jira UE-32318

Change 3031757 on 2016/06/28 by Chris.Babcock

	Remove unused methods from AndroidJNI header
	#ue4
	#android

Change 3032387 on 2016/06/29 by Allan.Bentham

	Rename android es31+aep -> glesdeferred.

Change 3032711 on 2016/06/29 by Allan.Bentham

	Rename GLSL_310_ES_EXT shader define:
	ES31_AEP_PROFILE -> ESDEFERRED_PROFILE
	bumped UE_SHADER_GLSL_310_ES_EXT_VER version number.

Change 3033698 on 2016/06/29 by Jack.Porter

	Merging //UE4/Dev-Main to Dev-Mobile (//UE4/Dev-Mobile)

Change 3034210 on 2016/06/30 by Steve.Cano

	Added a new AndroidRuntimeSettings variable that allows creation of installers for both Windows and Mac/Linux if set to true.

	#jira UE-32302

	#ue4
	#android

Change 3034530 on 2016/06/30 by Chris.Babcock

	Rename FManifestReader to FAndroidFileManifestReader in AndroidFile
	#jira UE-32679
	#ue4
	#android

Change 3034612 on 2016/06/30 by Steve.Cano

	Change Alpha from being set to a range of 0-255 to being in a range of 0-1 (which is the correct range of values)

	#jira UE-25325
	#ue4
	#android

Change 3034679 on 2016/06/30 by Chris.Babcock

	Fix tooltip (.command for mac, not .sh)
	#jira UE-32302
	#ue4
	#android

Change 3038881 on 2016/07/05 by Jack.Porter

	Package and launch on multiple Android devices simultaneously using the -Device=xxxxxxx+yyyyyyyy+zzzzzzzz format generated by a Project Launcher profile when you select multiple devices

	#jira UEMOB-115

Change 3039240 on 2016/07/06 by Jack.Porter

	TcpMessageTransport - connection-based message bus transport.

	#jira UEMOB-112
	#jira UEMOB-113

Change 3039252 on 2016/07/06 by Jack.Porter

	Enable messaging and session services and functional testing on Android when launched with -messaging
	Android device detection module support for adding port forwarding and connection announcement for TcpMessageTransport

	#jira UEMOB-112
	#jira UEMOB-113

Change 3039264 on 2016/07/06 by Jack.Porter

	Merging //UE4/Dev-Main to Dev-Mobile (//UE4/Dev-Mobile)

Change 3040041 on 2016/07/06 by Chris.Babcock

	Pass proper value to script generator functions
	#jira UE-32861
	#ue4
	#android

Change 3040890 on 2016/07/07 by Allan.Bentham

	Fix shadow crash
	#jira UE-32884

Change 3041458 on 2016/07/07 by Peter.Sauerbrei

	fix for IOS launch on failures

Change 3041542 on 2016/07/07 by Peter.Sauerbrei

	better fix for the multi-device deployment issue

Change 3041774 on 2016/07/07 by Steve.Cano

	Fixing crash that occurs when a games app id for Google Play is set before configuring the apk packaging. Also validating the value that is inserted and using it to override any values that have been hand-inserted into the GooglePlayAppID.xml

	#jira UE-16992
	#android
	#ue4

Change 3042222 on 2016/07/08 by Dmitriy.Dyomin

	Mobile packaging scenarious
	Added a wizard for creating launcher profiles (Android & IOS) for scenario: Minimal App + Downloadable content
	Added Archive step to launcher profiles to be able to store build product into specified directory
	Changes to a cooker to be able to pack DLC based with a different flavor to a release App
	Changes to DLC packaging to be able to build streaming data without chunking pak files
	#jira UEMOB-119

Change 3042244 on 2016/07/08 by Dmitriy.Dyomin

	Fixed crash in FTcpMessageTransportConnection::Stop

Change 3042270 on 2016/07/08 by Dmitriy.Dyomin

	GitHub #2320 : [ULevelStreamingKismet] Load Level Instance, Enables UE4 Users to create multiple transformed instances of a .umap without having to include in persistent level's list ? Rama
	contributed by: EverNewJoy
	#jira UE-29867

Change 3042449 on 2016/07/08 by Dmitriy.Dyomin

	Fixing Mac Editor build erros from CL# 3042222

Change 3042480 on 2016/07/08 by Allan.Bentham

	Add ES3.1 profile & compiler_glsl_es3_1 to shaders.

Change 3042481 on 2016/07/08 by Allan.Bentham

	hlslcc - ES3.1 changes.
	set ES3.1 version number to 310
	Do not use ES2 keywords for ES3.1.
	Generate Layout Locations for ES3.1
	bump version.

Change 3042483 on 2016/07/08 by Allan.Bentham

	Add mobile ES3.1 support.
	Recreates EGL and ES3.1 context during PlatformInitOpenGL if ES3.1 is required.

Change 3042485 on 2016/07/08 by Allan.Bentham

	Undo android XGE change.

Change 3042506 on 2016/07/08 by Dmitriy.Dyomin

	One more compile fix from CL# 3042222

Change 3044173 on 2016/07/10 by Dmitriy.Dyomin

	UAT: Added support for building target platforms with multiple cook flavors
	ex: -targetplatform=Android -cookflavor=ETC1+ETC2

Change 3044213 on 2016/07/11 by Dmitriy.Dyomin

	Fixed: Can't stream in a level whose name is a substring of another streaming level
	#jira UE-32999

Change 3044221 on 2016/07/11 by Jack.Porter

	Merging //UE4/Dev-Main to Dev-Mobile (//UE4/Dev-Mobile)

Change 3044815 on 2016/07/11 by Allan.Bentham

	Corrected NAME_GLSL_ES3_1_ANDROID format string.

Change 3046911 on 2016/07/12 by Chris.Babcock

	Add handling of OnTextChanged for virtual keyboard input on Android
	#jira UE-32348
	#ue4
	#android

Change 3046958 on 2016/07/12 by Chris.Babcock

	Rename some functions with Error in the name to prevent false coloring in the logs
	#jira UE-30541
	#ue4
	#android

Change 3047169 on 2016/07/12 by Chris.Babcock

	Return player ID and handle auth token for Google Play Games on Android (contributed by gameDNAstudio)
	#jira UE-30610
	#pr #2372
	#ue4
	#android

Change 3047406 on 2016/07/12 by Jack.Porter

	Add missing import to GameActivity.java

Change 3047442 on 2016/07/13 by Dmitriy.Dyomin

	Added: Mobile custom post-process
	Limitations: can fetch only from PostProcessInput0 (SceneColor) other scene textures are not supported. Does not support "Replacing the Tonemapper" blendable location.
	#jira UEMOB-147

Change 3047466 on 2016/07/13 by Dmitriy.Dyomin

	Disabled engine crash handler on Android, system crash handler works more reliably across different os versions/devices

Change 3047746 on 2016/07/13 by Jack.Porter

	Rename FBasePassFowardDynamicPointLightInfo

Change 3047778 on 2016/07/13 by Jack.Porter

	Missing file for rename FBasePassFowardDynamicPointLightInfo

Change 3047788 on 2016/07/13 by Allan.Bentham

	Fix incorrect TargetPlatformDescriptor string generation.

Change 3047790 on 2016/07/13 by Allan.Bentham

	Fixed half3x3 matrix use with ES3.1 glsl
	Fixed couple of interpolator precision mismatch.
	Fixed ES3.1 support detection issues

Change 3047816 on 2016/07/13 by Allan.Bentham

	Remove AndroidGL4 remnants.

Change 3048926 on 2016/07/13 by Chris.Babcock

	Added detection of Amazon Fire TV to disable requiring virtual joysticks
	#ue4
	#android

Change 3049335 on 2016/07/14 by Dmitriy.Dyomin

	Fixing UAT crash when packaging project for iOS

Change 3049390 on 2016/07/14 by Jack.Porter

	Disabled error for warning 4819 "The file contains a character that cannot be represented in the current code page (xxx). Save the file in Unicode format to prevent data loss"
	This is triggered by European characters and copyright symbols in source saved as latin-1 when compiling on non-US windows. Seen often in 3rd party headers, eg nvapi.

	#code_review: Ben.Marsh

Change 3049391 on 2016/07/14 by Jack.Porter

	Fixed incorrect comment order in CL 3049390

Change 3049545 on 2016/07/14 by Dmitriy.Dyomin

	Reworking some code from CL#3047442 to make static analizer happy

Change 3049626 on 2016/07/14 by Allan.Bentham

	Automatic CSM shader toggling
	#jira UE-27429

Change 3051574 on 2016/07/15 by Jack.Porter

	Support for lighting channels on Mobile
	- Multiple directional lights are supported in different channels but primitives are only affected by the directional light in the first channel they have set
	- CSM shadows from stationary or movable directional lights correctly follow their lighting channels
	- No channel limitations for dynamic point lights

	Notes:
	Removed mobile-specific directional light shadowing fields from View uniform buffer and mobile no longers uses SimpleDirectionalLight.
	Separate uniform buffers for mobile directional light are generated for each lighting channel.
	CSM culling information is now stored in FViewInfo and not per FVisibleLightViewInfo as the visibility bits are per view.

	#code_review Daniel.Wright
	#jira UEMOB-110

Change 3051699 on 2016/07/15 by Steve.Cano

	Preserve the original, pre-transformed input vertices for Slate shaders, which is required to properly do anti-aliasing (the ViewProjection-transformed values were causing the lines to not be drawn).

	#jira UE-20320
	#ue4
	#android

Change 3051744 on 2016/07/15 by Chris.Babcock

	Fix Android Vulkan include path checks (contributed by kodomastro)
	#jira UE-33311
	#PR #2602
	#ue4
	#android

Change 3052023 on 2016/07/15 by Chris.Babcock

	Fix shadowed variables

Change 3052110 on 2016/07/15 by Chris.Babcock

	Compile fixes for light channel support on mobile
	- missing template
	- accessor function for MobileDirectionalLights from scene

Change 3052242 on 2016/07/15 by Chris.Babcock

	Compile fixes for light channel support on mobile
	- removed dependency on C++14 feature

Change 3052730 on 2016/07/16 by Dmitriy.Dyomin

	Win32 build fix

Change 3053041 on 2016/07/17 by Jack.Porter

	Merging //UE4/Dev-Main to Dev-Mobile (//UE4/Dev-Mobile)

Change 3053054 on 2016/07/17 by Jack.Porter

	Changed use of old function ShouldUseDeferredRenderer() to new GetShadingPath()

Change 3053055 on 2016/07/17 by Jack.Porter

	Fixed local variable aliasing in unity build

Change 3053206 on 2016/07/18 by Jack.Porter

	Support ExecuteJavascript on iOS and Android
	Expose ExecuteJavascript to widget blueprint
	Fix ExecuteJavascript unicode string support on desktop platforms

	#jira UEMOB-152

Change 3053323 on 2016/07/18 by Dmitriy.Dyomin

	Added: Ability to set thread affinity for a device in Device Profiles (ex: +CVars=android.SetThreadAffinity=RT 0x02 GT 0x01)
	#jira UEMOB-107

Change 3053723 on 2016/07/18 by Jack.Porter

	Fix for UnrealTournamentProto.Automation.cs build errors

Change 3055090 on 2016/07/19 by Dmitriy.Dyomin

	Junk OnlineBlueprintSupport module binaries

[CL 3056789 by Jack Porter in Main branch]
2016-07-19 19:13:01 -04:00

1382 lines
51 KiB
C#

// Copyright 1998-2016 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;
static class IOSEnvVarNames
{
// Should we code sign when staging? (defaults to 1 if not present)
static public readonly string CodeSignWhenStaging = "uebp_CodeSignWhenStaging";
}
public class IOSPlatform : Platform
{
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";
}
// Run the integrated IPP code
// @todo ipp: How to log the output in a nice UAT way?
protected static int RunIPP(string IPPArguments)
{
List<string> Args = new List<string>();
StringBuilder Token = null;
int Index = 0;
bool bInQuote = false;
while (Index < IPPArguments.Length)
{
if (IPPArguments[Index] == '\"')
{
bInQuote = !bInQuote;
}
else if (IPPArguments[Index] == ' ' && !bInQuote)
{
if (Token != null)
{
Args.Add(Token.ToString());
Token = null;
}
}
else
{
if (Token == null)
{
Token = new StringBuilder();
}
Token.Append(IPPArguments[Index]);
}
Index++;
}
if (Token != null)
{
Args.Add(Token.ToString());
}
return RunIPP(Args.ToArray());
}
protected static int RunIPP(string[] Args)
{
return 5;//@TODO: Reintegrate IPP to IOS.Automation iPhonePackager.Program.RealMain(Args);
}
public override int RunCommand(string Command)
{
// run generic IPP commands through the interface
if (Command.StartsWith("IPP:"))
{
return RunIPP(Command.Substring(4));
}
// non-zero error code
return 4;
}
public virtual UnrealBuildTool.UEDeployIOS GetDeployHandler(FileReference InProject)
{
Console.WriteLine("Getting IOS Deploy()");
return new UnrealBuildTool.UEDeployIOS(InProject);
}
protected string MakeIPAFileName( UnrealTargetConfiguration TargetConfiguration, ProjectParams Params )
{
string ProjectIPA = Path.Combine(Path.GetDirectoryName(Params.RawProjectPath.FullName), "Binaries", PlatformName, (Params.Distribution ? "Distro_" : "") + Params.ShortProjectName);
if (TargetConfiguration != UnrealTargetConfiguration.Development)
{
ProjectIPA += "-" + PlatformType.ToString() + "-" + TargetConfiguration.ToString();
}
ProjectIPA += ".ipa";
return ProjectIPA;
}
protected string MakeExeFileName( UnrealTargetConfiguration TargetConfiguration, ProjectParams Params )
{
string ProjectIPA = Path.Combine(Path.GetDirectoryName(Params.RawProjectPath.FullName), "Binaries", PlatformName, Params.ShortProjectName);
if (TargetConfiguration != UnrealTargetConfiguration.Development)
{
ProjectIPA += "-" + PlatformType.ToString() + "-" + TargetConfiguration.ToString();
}
ProjectIPA += ".ipa";
return ProjectIPA;
}
// 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;
}
public override void Package(ProjectParams Params, DeploymentContext SC, int WorkingCL)
{
Log("Package {0}", Params.RawProjectPath);
// ensure the ue4game binary exists, if applicable
string FullExePath = CombinePaths(Path.GetDirectoryName(Params.ProjectGameExeFilename), SC.StageExecutables[0] + (UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac ? ".stub" : ""));
if (!SC.IsCodeBasedProject && !FileExists_NoExceptions(FullExePath))
{
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 UE4 project with your target configuration and platform.", FullExePath);
}
//@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)
{
// copy in all of the artwork and plist
var DeployHandler = GetDeployHandler(Params.RawProjectPath);
DeployHandler.PrepForUATPackageOrDeploy(Params.RawProjectPath,
Params.ShortProjectName,
Path.GetDirectoryName(Params.RawProjectPath.FullName),
CombinePaths(Path.GetDirectoryName(Params.ProjectGameExeFilename), SC.StageExecutables[0]),
CombinePaths(SC.LocalRoot, "Engine"),
Params.Distribution,
"",
false);
// figure out where to pop in the staged files
string AppDirectory = string.Format("{0}/Payload/{1}.app",
Path.GetDirectoryName(Params.ProjectGameExeFilename),
Path.GetFileNameWithoutExtension(Params.ProjectGameExeFilename));
// delete the old cookeddata
InternalUtils.SafeDeleteDirectory(AppDirectory + "/cookeddata", true);
InternalUtils.SafeDeleteFile(AppDirectory + "/ue4commandline.txt", true);
if (!Params.IterativeDeploy)
{
// copy the Staged files to the AppDirectory
string[] StagedFiles = Directory.GetFiles (SC.StageDirectory, "*", SearchOption.AllDirectories);
foreach (string Filename in StagedFiles)
{
string DestFilename = Filename.Replace (SC.StageDirectory, AppDirectory);
Directory.CreateDirectory (Path.GetDirectoryName (DestFilename));
InternalUtils.SafeCopyFile (Filename, DestFilename, true);
}
}
else
{
// copy just the root stage directory files
string[] StagedFiles = Directory.GetFiles (SC.StageDirectory, "*", SearchOption.TopDirectoryOnly);
foreach (string Filename in StagedFiles)
{
string DestFilename = Filename.Replace (SC.StageDirectory, AppDirectory);
Directory.CreateDirectory (Path.GetDirectoryName (DestFilename));
InternalUtils.SafeCopyFile (Filename, DestFilename, true);
}
}
}
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);
}
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))
{
UnrealBuildTool.IOSPlatformContext BuildPlatContext = new IOSPlatformContext(Params.RawProjectPath);
BuildPlatContext.SetUpProjectEnvironment();
Params.Provision = BuildPlatContext.MobileProvision;
}
if (String.IsNullOrEmpty(Params.Certificate))
{
UnrealBuildTool.IOSPlatformContext BuildPlatContext = new IOSPlatformContext(Params.RawProjectPath);
BuildPlatContext.SetUpProjectEnvironment();
Params.Certificate = BuildPlatContext.SigningCertificate;
}
var TargetConfiguration = SC.StageTargetConfigurations[0];
// Scheme name and configuration for code signing with Xcode project
string SchemeName = Params.IsCodeBasedProject ? Params.RawProjectPath.GetFileNameWithoutExtension() : "UE4";
string SchemeConfiguration = TargetConfiguration.ToString();
if (Params.Client)
{
SchemeConfiguration += " Client";
}
if (UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac)
{
var ProjectIPA = MakeIPAFileName(TargetConfiguration, Params);
var ProjectStub = Path.GetFullPath(Params.ProjectGameExeFilename);
// 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("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);
bCreatedIPA = true;
if (RemoteToolChain.bUseRPCUtil)
{
string IPPArguments = "RepackageFromStage \"" + (Params.IsCodeBasedProject ? Params.RawProjectPath.FullName : "Engine") + "\"";
IPPArguments += " -config " + TargetConfiguration.ToString();
IPPArguments += " -schemename " + SchemeName + " -schemeconfig \"" + SchemeConfiguration + "\"";
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";
}
}
IPPArguments += (cookonthefly ? " -cookonthefly" : "");
IPPArguments += " -stagedir \"" + CombinePaths(Params.BaseStageDirectory, PlatformName) + "\"";
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);
}
else
{
List<string> IPPArguments = new List<string>();
IPPArguments.Add("RepackageFromStage");
IPPArguments.Add(Params.IsCodeBasedProject ? Params.RawProjectPath.FullName : "Engine");
IPPArguments.Add("-config");
IPPArguments.Add(TargetConfiguration.ToString());
IPPArguments.Add("-schemename");
IPPArguments.Add(SchemeName);
IPPArguments.Add("-schemeconfig");
IPPArguments.Add("\"" + SchemeConfiguration + "\"");
if (TargetConfiguration == UnrealTargetConfiguration.Shipping)
{
IPPArguments.Add("-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.Add("-bundlename");
IPPArguments.Add(Params.BundleName);
}
if (bNeedToSign)
{
IPPArguments.Add("-sign");
}
if (cookonthefly)
{
IPPArguments.Add(" -cookonthefly");
}
IPPArguments.Add(" -stagedir");
IPPArguments.Add(CombinePaths(Params.BaseStageDirectory, "IOS"));
IPPArguments.Add(" -project");
IPPArguments.Add(Params.RawProjectPath.FullName);
if (Params.IterativeDeploy)
{
IPPArguments.Add(" -iterate");
}
if (!string.IsNullOrEmpty(Params.Provision))
{
IPPArguments.Add(" -provision");
IPPArguments.Add(Params.Provision);
}
if (!string.IsNullOrEmpty(Params.Certificate))
{
IPPArguments.Add(" -certificate");
IPPArguments.Add(Params.Certificate);
}
if (RunIPP(IPPArguments.ToArray()) != 0)
{
throw new AutomationException("IPP Failed");
}
}
}
// 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<string>
{
ProjectIPA
};
// check in the .ipa along with everything else
UE4Build.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(Params.ProjectGameExeFilename), Params.IsCodeBasedProject ? Params.ShortProjectName : Path.GetFileNameWithoutExtension(Params.ProjectGameExeFilename), Params.RawProjectPath, SC.StageTargetConfigurations[0], SC.LocalRoot, Params.ShortProjectName, Path.GetDirectoryName(Params.RawProjectPath.FullName), SC.IsCodeBasedProject, Params.Distribution, Params.Provision, Params.Certificate, SchemeName, SchemeConfiguration);
// now generate the ipa
PackageIPA(Path.GetDirectoryName(Params.ProjectGameExeFilename), Params.IsCodeBasedProject ? Params.ShortProjectName : Path.GetFileNameWithoutExtension(Params.ProjectGameExeFilename), Params.ShortProjectName, Path.GetDirectoryName(Params.RawProjectPath.FullName), SC.StageTargetConfigurations[0], Params.Distribution);
}
}
PrintRunTime();
}
private string EnsureXcodeProjectExists(FileReference RawProjectPath, string LocalRoot, string ShortProjectName, string ProjectRoot, bool IsCodeBasedProject, out bool bWasGenerated)
{
// first check for ue4.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";
// If engine is installed then UBT doesn't need to be built
if (Automation.IsEngineInstalled())
{
// Get the path to UBT
string InstalledUBT = CombinePaths(CmdEnv.LocalRoot, "Engine/Binaries/DotNET/UnrealBuildTool.exe");
Arguments = "-XcodeProjectFile " + Arguments;
RunUBT(CmdEnv, InstalledUBT, 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;
}
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 SchemeName = null, string SchemeConfiguration = null)
{
// 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 (!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("<key>UUID</key>");
if (idx > 0)
{
idx = AllText.IndexOf("<string>", idx);
if (idx > 0)
{
idx += "<string>".Length;
UUID = AllText.Substring(idx, AllText.IndexOf("</string>", idx) - idx);
Arguments += " PROVISIONING_PROFILE=" + UUID;
}
}
}
}
ProcessResult 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");
}
}
private void PackageIPA(string BaseDirectory, string GameName, string ProjectName, string ProjectDirectory, UnrealTargetConfiguration TargetConfig, bool Distribution = false)
{
// create the ipa
string IPAName = CombinePaths(ProjectDirectory, "Binaries", PlatformName, (Distribution ? "Distro_" : "") + ProjectName + (TargetConfig != UnrealTargetConfiguration.Development ? ("-" + PlatformName + "-" + TargetConfig.ToString()) : "") + ".ipa");
// 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;
// set the compression level
if (Distribution)
{
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(GameName, StringComparison.InvariantCultureIgnoreCase))
{
bIsExecutable = true;
}
if (bIsExecutable)
{
// 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 the icons/launch screens from the engine
{
string SourcePath = CombinePaths(SC.LocalRoot, "Engine", "Build", "IOS", "Resources", "Graphics");
SC.StageFiles(StagedFileType.NonUFS, SourcePath, "*.png", false, null, "", true, false);
}
// copy any additional framework assets that will be needed at runtime
{
string SourcePath = CombinePaths( ( SC.IsCodeBasedProject ? SC.ProjectRoot : SC.LocalRoot + "\\Engine" ), "Intermediate", "IOS", "FrameworkAssets" );
if ( Directory.Exists( SourcePath ) )
{
SC.StageFiles( StagedFileType.NonUFS, SourcePath, "*.*", true, null, "", true, false );
}
}
// copy the icons/launch screens from the game (may stomp the engine copies)
{
string SourcePath = CombinePaths(SC.ProjectRoot, "Build", "IOS", "Resources", "Graphics");
SC.StageFiles(StagedFileType.NonUFS, SourcePath, "*.png", false, null, "", true, false);
}
// 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))
{
string SourcePath = CombinePaths((SC.IsCodeBasedProject ? SC.ProjectRoot : SC.LocalRoot + "/Engine"), "Intermediate", PlatformName);
string TargetPListFile = Path.Combine(SourcePath, (SC.IsCodeBasedProject ? SC.ShortProjectName : "UE4Game") + "-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.UnrealBuildTool.SetRemoteIniPath(SC.ProjectRoot);
}
GetDeployHandler(new FileReference(SC.ProjectRoot)).GeneratePList((SC.IsCodeBasedProject ? SC.ProjectRoot : SC.LocalRoot + "/Engine"), !SC.IsCodeBasedProject, (SC.IsCodeBasedProject ? SC.ShortProjectName : "UE4Game"), SC.ShortProjectName, SC.LocalRoot + "/Engine", (SC.IsCodeBasedProject ? SC.ProjectRoot : SC.LocalRoot + "/Engine") + "/Binaries/" + PlatformName + "/Payload/" + (SC.IsCodeBasedProject ? SC.ShortProjectName : "UE4Game") + ".app");
}
SC.StageFiles(StagedFileType.NonUFS, SourcePath, Path.GetFileName(TargetPListFile), false, null, "", false, false, "Info.plist");
}
}
// copy the movies from the project
{
SC.StageFiles(StagedFileType.NonUFS, CombinePaths(SC.ProjectRoot, "Build/IOS/Resources/Movies"), "*", false, null, "", true, false);
SC.StageFiles(StagedFileType.NonUFS, CombinePaths(SC.ProjectRoot, "Content/Movies"), "*", true, null, "", true, false);
}
}
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 );
// verify the .ipa exists
if (!FileExists(ProjectIPA))
{
throw new AutomationException("ARCHIVE FAILED - {0} was not found", ProjectIPA);
}
ConfigCacheIni PlatformGameConfig;
bool bXCArchive = false;
if (Params.EngineConfigs.TryGetValue(SC.StageTargetPlatform.PlatformType, out PlatformGameConfig))
{
PlatformGameConfig.GetBool("/Script/IOSRuntimeSettings.IOSRuntimeSettings", "bGenerateXCArchive", out bXCArchive);
}
if (bXCArchive && Utils.IsRunningOnMono)
{
// Always put the archive in the current user's Library/Developer/Xcode/Archives path
WindowsIdentity id = WindowsIdentity.GetCurrent();
string ArchivePath = "/Users/" + id.Name + "/Library/Developer/Xcode/Archives";
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";
using (ZipFile Zip = new ZipFile(ProjectIPA))
{
Zip.ExtractAll(ArchivePath, ExtractExistingFileAction.OverwriteSilently);
List<string> Dirs = new List<string>(Directory.EnumerateDirectories(Path.Combine(ArchivePath, "Payload"), "*.app"));
AppName = Dirs[0].Substring(Dirs[0].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 = MakeExeFileName( TargetConfiguration, Params );
string dSYMName = (SC.IsCodeBasedProject ? Path.GetFileNameWithoutExtension(ProjectExe) : "UE4Game") + ".dSYM";
string dSYMSrcPath = Path.Combine(SC.ProjectBinariesFolder, dSYMName);
if(DirectoryExists(new string[] { dSYMSrcPath } ))
{
// Create the dsyms archive folder
CreateDirectory(Path.Combine(ArchiveName, "dSYMs"));
string dSYMDstPath = Path.Combine(ArchiveName, "dSYMs", dSYMName);
// /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) : "UE4Game"), dSYMDstPath);
}
else if (File.Exists(dSYMSrcPath))
{
// Create the dsyms archive folder
CreateDirectory(Path.Combine(ArchiveName, "dSYMs"));
string dSYMDstPath = Path.Combine(ArchiveName, "dSYMs", dSYMName);
CopyFile_NoExceptions(dSYMSrcPath, dSYMDstPath);
}
// copy in the bitcode symbol maps if found
string[] bcmapfiles = Directory.GetFiles(SC.ProjectBinariesFolder, "*.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) : "";
// bundle identifier
int index = OldPListData.IndexOf("CFBundleIdentifier");
index = OldPListData.IndexOf("<string>", index) + 8;
int length = OldPListData.IndexOf("</string>", index) - index;
string BundleIdentifier = OldPListData.Substring(index, length);
// short version
index = OldPListData.IndexOf("CFBundleShortVersionString");
index = OldPListData.IndexOf("<string>", index) + 8;
length = OldPListData.IndexOf("</string>", index) - index;
string BundleShortVersion = OldPListData.Substring(index, length);
// bundle version
index = OldPListData.IndexOf("CFBundleVersion");
index = OldPListData.IndexOf("<string>", index) + 8;
length = OldPListData.IndexOf("</string>", index) - index;
string BundleVersion = OldPListData.Substring(index, length);
// 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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
Text.AppendLine("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
Text.AppendLine("<plist version=\"1.0\">");
Text.AppendLine("<dict>");
Text.AppendLine("\t<key>ApplicationProperties</key>");
Text.AppendLine("\t<dict>");
Text.AppendLine("\t\t<key>ApplicationPath</key>");
Text.AppendLine("\t\t<string>Applications/" + AppName + "</string>");
Text.AppendLine("\t\t<key>CFBundleIdentifier</key>");
Text.AppendLine(string.Format("\t\t<string>{0}</string>", BundleIdentifier));
Text.AppendLine("\t\t<key>CFBundleShortVersionString</key>");
Text.AppendLine(string.Format("\t\t<string>{0}</string>", BundleShortVersion));
Text.AppendLine("\t\t<key>CFBundleVersion</key>");
Text.AppendLine(string.Format("\t\t<string>{0}</string>", BundleVersion));
Text.AppendLine("\t\t<key>SigningIdentity</key>");
Text.AppendLine(string.Format("\t\t<string>{0}</string>", Params.Certificate));
Text.AppendLine("\t</dict>");
Text.AppendLine("\t<key>ArchiveVersion</key>");
Text.AppendLine("\t<integer>2</integer>");
Text.AppendLine("\t<key>CreationDate</key>");
Text.AppendLine(string.Format("\t<date>{0}</date>", TimeStamp));
Text.AppendLine("\t<key>DefaultToolchainInfo</key>");
Text.AppendLine("\t<dict>");
Text.AppendLine("\t\t<key>DisplayName</key>");
Text.AppendLine("\t\t<string>Xcode 7.3 Default</string>");
Text.AppendLine("\t\t<key>Identifier</key>");
Text.AppendLine("\t\t<string>com.apple.dt.toolchain.XcodeDefault</string>");
Text.AppendLine("\t</dict>");
Text.AppendLine("\t<key>Name</key>");
Text.AppendLine(string.Format("\t<string>{0}</string>", SC.ShortProjectName));
Text.AppendLine("\t<key>SchemeName</key>");
Text.AppendLine(string.Format("\t<string>{0}</string>", SC.ShortProjectName));
Text.AppendLine("</dict>");
Text.AppendLine("</plist>");
File.WriteAllText(Path.Combine(ArchiveName, "Info.plist"), Text.ToString());
}
else if (bXCArchive && !Utils.IsRunningOnMono)
{
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<string> UFSManifests, out List<string> 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<string>();
NonUFSManifests = new List<string>();
var DeployServer = CombinePaths(CmdEnv.LocalRoot, "Engine/Binaries/DotNET/IOS/DeploymentServer.exe");
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("<string>", Pos) + 8;
int EndPos = Contents.IndexOf("</string>", Pos);
BundleIdentifier = Contents.Substring(Pos, EndPos - Pos);
}
RunAndLog(CmdEnv, DeployServer, "Backup -file \"" + CombinePaths(Params.BaseStageDirectory, PlatformName, SC.UFSDeployedManifestFileName) + "\" -file \"" + CombinePaths(Params.BaseStageDirectory, PlatformName, SC.NonUFSDeployedManifestFileName) + "\"" + (String.IsNullOrEmpty(Params.DeviceNames[0]) ? "" : " -device " + Params.DeviceNames[0]) + " -bundle " + BundleIdentifier);
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);
}
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;
}
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);
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("<string>", Pos) + 8;
int EndPos = Contents.IndexOf("</string>", 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 DeployServer = CombinePaths(CmdEnv.LocalRoot, "Engine/Binaries/DotNET/IOS/DeploymentServer.exe");
// 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, DeployServer, "Install -ipa \"" + Path.GetFullPath(StagedIPA) + "\"" + (String.IsNullOrEmpty(Params.DeviceNames[0]) ? "" : " -device " + Params.DeviceNames[0]) + AdditionalCommandline);
}
if (Params.IterativeDeploy)
{
// push over the changed files
RunAndLog(CmdEnv, DeployServer, "Deploy -manifest \"" + CombinePaths(Params.BaseStageDirectory, PlatformName, DeploymentContext.UFSDeployDeltaFileName + (Params.Devices.Count == 0 ? "" : Params.DeviceNames[0])) + "\"" + (Params.Devices.Count == 0 ? "" : " -device " + Params.DeviceNames[0]) + AdditionalCommandline + " -bundle " + BundleIdentifier);
}
Directory.SetCurrentDirectory (CurrentDir);
PrintRunTime();
}
public override string GetCookPlatform(bool bDedicatedServer, bool bIsClientOnly)
{
return "IOS";
}
public override bool DeployPakInternalLowerCaseFilenames()
{
return false;
}
public override bool DeployLowerCaseFilenames(bool bUFSFile)
{
// we shouldn't modify the case on files like Info.plist or the icons
return bUFSFile;
}
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 string Remap(string Dest)
{
return "cookeddata/" + Dest;
}
public override List<string> GetDebugFileExtentions()
{
return new List<string> { ".dsym" };
}
public override ProcessResult RunClient(ERunOptions ClientRunFlags, string ClientApp, string ClientCmdLine, ProjectParams Params)
{
if (UnrealBuildTool.BuildHostPlatform.Current.Platform == UnrealTargetPlatform.Mac)
{
if (Params.Devices.Count != 1)
{
throw new AutomationException("Can only run on a single specified device, but {0} were specified", Params.Devices.Count);
}
/* string AppDirectory = string.Format("{0}/Payload/{1}.app",
Path.GetDirectoryName(Params.ProjectGameExeFilename),
Path.GetFileNameWithoutExtension(Params.ProjectGameExeFilename));
string GameName = Path.GetFileNameWithoutExtension (ClientApp);
if (GameName.Contains ("-IOS-"))
{
GameName = GameName.Substring (0, GameName.IndexOf ("-IOS-"));
}
string GameApp = AppDirectory + "/" + GameName;
bWasGenerated = false;
XcodeProj = EnsureXcodeProjectExists (Params.RawProjectPath, CmdEnv.LocalRoot, Params.ShortProjectName, GetDirectoryName(Params.RawProjectPath), Params.IsCodeBasedProject, out bWasGenerated);
string Arguments = "UBT_NO_POST_DEPLOY=true /usr/bin/xcrun xcodebuild test -project \"" + XcodeProj + "\"";
Arguments += " -scheme '";
Arguments += GameName;
Arguments += " - iOS'";
Arguments += " -configuration " + Params.ClientConfigsToBuild [0].ToString();
Arguments += " -destination 'platform=iOS,id=" + Params.Device.Substring(Params.Device.IndexOf("@")+1) + "'";
Arguments += " TEST_HOST=\"";
Arguments += GameApp;
Arguments += "\" BUNDLE_LOADER=\"";
Arguments += GameApp + "\"";*/
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("<string>", Pos) + 8;
int EndPos = Contents.IndexOf("</string>", Pos);
BundleIdentifier = Contents.Substring(Pos, EndPos - Pos);
}
string Arguments = "/usr/bin/instruments";
Arguments += " -w '" + Params.DeviceNames[0] + "'";
Arguments += " -t 'Activity Monitor'";
Arguments += " -D \"" + Params.BaseStageDirectory + "/" + PlatformName + "/launch.trace\"";
Arguments += " '" + BundleIdentifier + "'";
ProcessResult ClientProcess = Run ("/usr/bin/env", Arguments, null, ClientRunFlags | ERunOptions.NoWaitForExit);
return ClientProcess;
}
else
{
ProcessResult Result = new ProcessResult("DummyApp", null, false, null);
Result.ExitCode = 0;
return Result;
}
}
public override void PostRunClient(ProcessResult 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>" + Key + "</key>");
}
// output value
if (Value is Array)
{
Text.AppendLine (Indent + "<array>");
Array ValArray = Value as Array;
foreach (var Item in ValArray)
{
AppendKeyValue (Text, null, Item, Level + 1);
}
Text.AppendLine (Indent + "</array>");
}
else if (Value is Dictionary<string, object>)
{
Text.AppendLine (Indent + "<dict>");
Dictionary<string,object> ValDict = Value as Dictionary<string, object>;
foreach (var Item in ValDict)
{
AppendKeyValue (Text, Item.Key, Item.Value, Level + 1);
}
Text.AppendLine (Indent + "</dict>");
}
else if (Value is string)
{
Text.AppendLine (Indent + "<string>" + Value + "</string>");
}
else if (Value is bool)
{
if ((bool)Value == true)
{
Text.AppendLine (Indent + "<true/>");
}
else
{
Text.AppendLine (Indent + "<false/>");
}
}
else
{
Console.WriteLine ("PLIST: Unknown array item type");
}
return Text;
}
private static void GeneratePlist(Dictionary<string, object> KeyValues, string PlistFile)
{
// generate the plist file
StringBuilder Text = new StringBuilder();
// boiler plate top
Text.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
Text.AppendLine("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
Text.AppendLine("<plist version=\"1.0\">");
Text.AppendLine("<dict>");
foreach (var KeyValue in KeyValues)
{
AppendKeyValue(Text, KeyValue.Key, KeyValue.Value, 1);
}
Text.AppendLine("</dict>");
Text.AppendLine("</plist>");
// 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<string, object> KeyValues = new Dictionary<string, object> ();
KeyValues.Add ("CFBundleIdentifier", BundleIdentifier);
KeyValues.Add ("Tags", Tags);
GeneratePlist(KeyValues, CombinePaths(AssetDir, "Info.plist"));
}
private static void GenerateAssetPackManifestPlist(KeyValuePair<string, string>[] ChunkData, string AssetDir)
{
Dictionary<string, object>[] Resources = new Dictionary<string, object>[ChunkData.Length];
for (int i = 0; i < ChunkData.Length; ++i)
{
Dictionary<string, object> Data = new Dictionary<string, object> ();
Data.Add ("URL", CombinePaths ("OnDemandResources", ChunkData[i].Value));
Data.Add ("bundleKey", ChunkData [i].Key);
Data.Add ("isStreamable", false);
Resources [i] = Data;
}
Dictionary<string, object> KeyValues = new Dictionary<string, object> ();
KeyValues.Add ("resources", Resources);
GeneratePlist(KeyValues, CombinePaths(AssetDir, "AssetPackManifest.plist"));
}
private static void GenerateOnDemandResourcesPlist(KeyValuePair<string, string>[] ChunkData, string AssetDir)
{
Dictionary<string, object> RequestTags = new Dictionary<string, object> ();
Dictionary<string, object> AssetPacks = new Dictionary<string, object> ();
Dictionary<string, object> Requests = new Dictionary<string, object> ();
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<string, object> Packs = new Dictionary<string, object> ();
Packs.Add ("NSAssetPacks", new string[] { ChunkData [i].Key });
Requests.Add (ChunkName, Packs);
}
Dictionary<string, object> KeyValues = new Dictionary<string, object> ();
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("<string>", Pos) + 8;
int EndPos = Contents.IndexOf("</string>", 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<string, string>[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<string, string>(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, "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);
// generate the OnDemandResources.plist
GenerateOnDemandResourcesPlist (ChunkData, SC.StageDirectory);
}
}
public override bool StageMovies
{
get { return false; }
}
public override bool RequiresPackageToDeploy
{
get { return true; }
}
public override List<string> GetFilesForCRCCheck()
{
List<string> FileList = base.GetFilesForCRCCheck();
FileList.Add("Info.plist");
return FileList;
}
public override bool SupportsMultiDeviceDeploy
{
get
{
return true;
}
}
#region Hooks
public override void PreBuildAgenda(UE4Build Build, UE4Build.BuildAgenda Agenda)
{
if (UnrealBuildTool.BuildHostPlatform.Current.Platform != UnrealTargetPlatform.Mac)
{
Agenda.DotNetProjects.Add(@"Engine\Source\Programs\IOS\MobileDeviceInterface\MobileDeviceInterface.csproj");
Agenda.DotNetProjects.Add(@"Engine\Source\Programs\IOS\iPhonePackager\iPhonePackager.csproj");
Agenda.DotNetProjects.Add(@"Engine\Source\Programs\IOS\DeploymentServer\DeploymentServer.csproj");
}
}
#endregion
}