Files
UnrealEngine/Engine/Source/Programs/IOS/iPhonePackager/CookTime.cs
2025-05-18 13:04:45 +08:00

386 lines
13 KiB
C#

/**
* Copyright Epic Games, Inc. All Rights Reserved.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using Ionic.Zip;
using Ionic.Zlib;
namespace iPhonePackager
{
/**
* Operations performed done at cook time - there should be no calls to the Mac here
*/
public class CookTime
{
/// <summary>
/// List of files being inserted or updated in the Zip
/// </summary>
static private HashSet<string> FilesBeingModifiedToPrintOut = new HashSet<string>();
/**
* Create and open a work IPA file
*/
static private ZipFile SetupWorkIPA()
{
string ReferenceZipPath = Config.GetIPAPathForReading(".stub");
string WorkIPA = Config.GetIPAPath(".ipa");
return CreateWorkingIPA(ReferenceZipPath, WorkIPA);
}
/// <summary>
/// Creates a copy of a source IPA to a working path and opens it up as a Zip for further modifications
/// </summary>
static private ZipFile CreateWorkingIPA(string SourceIPAPath, string WorkIPAPath)
{
FileInfo ReferenceInfo = new FileInfo(SourceIPAPath);
if (!ReferenceInfo.Exists)
{
Program.Error(String.Format("Failed to find stub IPA '{0}'", SourceIPAPath));
return null;
}
else
{
Program.Log(String.Format("Loaded stub IPA from '{0}' ...", SourceIPAPath));
}
if (Program.GameName == "UnrealGame")
{
WorkIPAPath = Config.RemapIPAPath(".ipa");
}
// Make sure there are no stale working copies around
FileOperations.DeleteFile(WorkIPAPath);
// Create a working copy of the IPA
FileOperations.CopyRequiredFile(SourceIPAPath, WorkIPAPath);
// Open up the zip file
ZipFile Stub = ZipFile.Read(WorkIPAPath);
// modern xcode uses Foo-IOS-Config style .app directories, so don't assume GameName, pull it out of of the zip
Config.AppDirectoryInZIP = FileOperations.ZipFileSystem.FindRootPathInIPA(Stub);
// Do a few quick spot checks to catch problems that may have occurred earlier
bool bHasCodeSignature = Stub[Config.AppDirectoryInZIP + "/_CodeSignature/CodeResources"] != null;
if (!bHasCodeSignature)
{
Program.Error("Stub IPA does not appear to be signed correctly (missing _CodeSignature/CodeResources)");
Program.ReturnCode = (int)ErrorCodes.Error_StubNotSignedCorrectly;
}
// Set encoding to support unicode filenames
Stub.AlternateEncodingUsage = ZipOption.Always;
Stub.AlternateEncoding = Encoding.UTF8;
return Stub;
}
/// <summary>
/// Extracts Info.plist from a Zip
/// </summary>
static private string ExtractEmbeddedPList(ZipFile Zip)
{
// Extract the existing Info.plist
string PListPathInZIP = String.Format("{0}/Info.plist", Config.AppDirectoryInZIP);
if (Zip[PListPathInZIP] == null)
{
Program.Error("Failed to find Info.plist in IPA (cannot update plist version)");
Program.ReturnCode = (int)ErrorCodes.Error_IPAMissingInfoPList;
return "";
}
else
{
// Extract the original into a temporary directory
string TemporaryName = Path.GetTempFileName();
FileStream OldFile = File.OpenWrite(TemporaryName);
Zip[PListPathInZIP].Extract(OldFile);
OldFile.Close();
// Read the text and delete the temporary copy
string PListSource = File.ReadAllText(TemporaryName, Encoding.UTF8);
File.Delete(TemporaryName);
return PListSource;
}
}
static private void CopyEngineOrGameFiles(string Subdirectory, string Filter)
{
// build the matching paths
string EngineDir = Path.Combine(Config.EngineBuildDirectory + Subdirectory);
string GameDir = Path.Combine(Config.BuildDirectory + Subdirectory);
// find the files in the engine directory
string[] EngineFiles = Directory.GetFiles(EngineDir, Filter);
if (Directory.Exists(GameDir))
{
string[] GameFiles = Directory.GetFiles(GameDir, Filter);
// copy all files from game
foreach (string GameFilename in GameFiles)
{
string DestFilename = Path.Combine(Config.PayloadDirectory, Path.GetFileName(GameFilename));
// copy the game verison instead of engine version
FileOperations.CopyRequiredFile(GameFilename, DestFilename);
}
}
// look for matching engine files that weren't in game
foreach (string EngineFilename in EngineFiles)
{
string GameFilename = Path.Combine(GameDir, Path.GetFileName(EngineFilename));
string DestFilename = Path.Combine(Config.PayloadDirectory, Path.GetFileName(EngineFilename));
if (!File.Exists(GameFilename))
{
// copy the game verison instead of engine version
FileOperations.CopyRequiredFile(EngineFilename, DestFilename);
}
}
}
static public void CopySignedFiles()
{
string TargetName = Config.GetTargetName();
string NameDecoration;
if (Program.GameConfiguration == "Development")
{
NameDecoration = Program.Architecture;
}
else
{
NameDecoration = "-" + Config.OSString + "-" + Program.GameConfiguration + Program.Architecture;
}
// Copy and un-decorate the binary name
FileOperations.CopyFiles(Config.BinariesDirectory, Config.PayloadDirectory, "<PAYLOADDIR>", TargetName + NameDecoration, null);
FileOperations.RenameFile(Config.PayloadDirectory, TargetName + NameDecoration, TargetName);
FileOperations.CopyNonEssentialFile(
Path.Combine(Config.BinariesDirectory, TargetName + NameDecoration + ".app.dSYM.zip"),
Path.Combine(Config.PCStagingRootDir, TargetName + NameDecoration + ".app.dSYM.zip.datecheck")
);
}
/**
* Callback for setting progress when saving zip file
*/
static private void UpdateSaveProgress(object Sender, SaveProgressEventArgs Event)
{
if (Event.EventType == ZipProgressEventType.Saving_BeforeWriteEntry)
{
if (FilesBeingModifiedToPrintOut.Contains(Event.CurrentEntry.FileName))
{
Program.Log(" ... Packaging '{0}'", Event.CurrentEntry.FileName);
}
}
}
/// <summary>
/// Updates the version string and then applies the settings in the user overrides plist
/// </summary>
/// <param name="Info"></param>
public static void UpdateVersion(Utilities.PListHelper Info)
{
// Update the minor version number if the current one is older than the version tracker file
// Assuming that the version will be set explicitly in the overrides file for distribution
VersionUtilities.UpdateMinorVersion(Info);
// Mark the type of build (development or distribution)
Info.SetString("EpicPackagingMode", Config.bForDistribution ? "Distribution" : "Development");
}
/**
* Using the stub IPA previously compiled on the Mac, create a new IPA with assets
*/
static public void RepackageIPAFromStub()
{
if (string.IsNullOrEmpty(Config.RepackageStagingDirectory) || !Directory.Exists(Config.RepackageStagingDirectory))
{
Program.Error("Directory specified with -stagedir could not be found!");
return;
}
DateTime StartTime = DateTime.Now;
CodeSignatureBuilder CodeSigner = null;
// Clean the staging directory
Program.ExecuteCommand("Clean", null);
// Create a copy of the IPA so as to not trash the original
ZipFile Zip = SetupWorkIPA();
if (Zip == null)
{
return;
}
FileOperations.ZipFileSystem FileSystem = new FileOperations.ZipFileSystem(Zip);
// Check for a staged plist that needs to be merged into the main one
{
// Determine if there is a staged one we should try to use instead
string PossiblePList = Path.Combine(Config.RepackageStagingDirectory, "Info.plist");
if (File.Exists(PossiblePList))
{
if (Config.bPerformResignWhenRepackaging)
{
Program.Log("Found Info.plist ({0}) in stage, which will be merged in with stub plist contents", PossiblePList);
// Merge the two plists, using the staged one as the authority when they conflict
byte[] StagePListBytes = File.ReadAllBytes(PossiblePList);
string StageInfoString = Encoding.UTF8.GetString(StagePListBytes);
byte[] StubPListBytes = FileSystem.ReadAllBytes("Info.plist");
Utilities.PListHelper StubInfo = new Utilities.PListHelper(Encoding.UTF8.GetString(StubPListBytes));
// don't overwrite the Exe name in the plist, because we may be merging in Development staged data
// with a Shipping binary (or whatever combination), and modern uses different-named executables for each config
StubInfo.MergePlistIn(StageInfoString, new HashSet<string> { "CFBundleExecutable" } );
// Write it back to the cloned stub, where it will be used for all subsequent actions
byte[] MergedPListBytes = Encoding.UTF8.GetBytes(StubInfo.SaveToString());
FileSystem.WriteAllBytes("Info.plist", MergedPListBytes);
}
else
{
Program.Warning("Found Info.plist ({0}) in stage that will be ignored; IPP cannot combine it with the stub plist since -sign was not specified", PossiblePList);
}
}
}
// Get the name of the executable file
string CFBundleExecutable;
{
// Load the .plist from the stub
byte[] RawInfoPList = FileSystem.ReadAllBytes("Info.plist");
Utilities.PListHelper Info = new Utilities.PListHelper(Encoding.UTF8.GetString(RawInfoPList));
// Get the name of the executable file
if (!Info.GetString("CFBundleExecutable", out CFBundleExecutable))
{
throw new InvalidDataException("Info.plist must contain the key CFBundleExecutable");
}
}
// Tell the file system about the executable file name so that we can set correct attributes on
// the file when zipping it up
FileSystem.ExecutableFileName = CFBundleExecutable;
// Prepare for signing if requested
if (Config.bPerformResignWhenRepackaging)
{
// Start the resign process (load the mobileprovision and info.plist, find the cert, etc...)
CodeSigner = new CodeSignatureBuilder();
CodeSigner.FileSystem = FileSystem;
CodeSigner.PrepareForSigning();
// Merge in any user overrides that exist
UpdateVersion(CodeSigner.Info);
}
// Empty the current staging directory
FileOperations.DeleteDirectory(new DirectoryInfo(Config.PCStagingRootDir));
// we will zip files in the pre-staged payload dir
string ZipSourceDir = Config.RepackageStagingDirectory;
// Save the zip
Program.Log("Saving IPA ...");
FilesBeingModifiedToPrintOut.Clear();
Zip.SaveProgress += UpdateSaveProgress;
Zip.CompressionLevel = (Ionic.Zlib.CompressionLevel)Config.RecompressionSetting;
// Add all of the payload files, replacing existing files in the stub IPA if necessary (should only occur for icons)
{
string SourceDir = Path.GetFullPath(ZipSourceDir);
string[] PayloadFiles = Directory.GetFiles(SourceDir, "*.*", Config.bIterate ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories);
foreach (string Filename in PayloadFiles)
{
// Get the relative path to the file (this implementation only works because we know the files are all
// deeper than the base dir, since they were generated from a search)
string AbsoluteFilename = Path.GetFullPath(Filename);
string RelativeFilename = AbsoluteFilename.Substring(SourceDir.Length + 1).Replace('\\', '/');
string ZipAbsolutePath = String.Format("{0}/{1}",
Config.AppDirectoryInZIP,
RelativeFilename);
Stream FileContents = File.OpenRead(AbsoluteFilename);
if (FileContents.Length == 0)
{
// Zero-length files added by Ionic cause installation/upgrade to fail on device with error 0xE8000050
// We store a single byte in the files as a workaround for now
byte[] DummyContents = new byte[1];
DummyContents[0] = 0;
FileContents = new MemoryStream(DummyContents);
}
FileSystem.WriteStream(RelativeFilename, FileContents);
if ((FileContents.Length >= 1024 * 1024) || (Config.bVerbose))
{
FilesBeingModifiedToPrintOut.Add(ZipAbsolutePath);
}
}
}
// Re-sign the executable if there is a signing context
if (CodeSigner != null)
{
if (Config.OverrideBundleName != null)
{
CodeSigner.Info.SetString("CFBundleDisplayName", Config.OverrideBundleName);
string CFBundleIdentifier;
if (CodeSigner.Info.GetString("CFBundleIdentifier", out CFBundleIdentifier))
{
CodeSigner.Info.SetString("CFBundleIdentifier", CFBundleIdentifier + "_" + Config.OverrideBundleName);
}
}
CodeSigner.PerformSigning();
}
// Stick in the iTunesArtwork PNG if available
string iTunesArtworkPath = Path.Combine(Config.BuildDirectory, "iTunesArtwork");
if (File.Exists(iTunesArtworkPath))
{
Zip.UpdateFile(iTunesArtworkPath, "");
}
// Save the Zip
Program.Log("Compressing files into IPA (-compress={1}).{0}", Config.bVerbose ? "" : " Only large files will be listed next, but other files are also being packaged.", Config.RecompressionSetting);
FileSystem.Close();
TimeSpan ZipLength = DateTime.Now - StartTime;
FileInfo FinalZipInfo = new FileInfo(Zip.Name);
Program.Log(String.Format("Finished repackaging into {2:0.00} MB IPA, written to '{0}' (took {1:0.00} s for all steps)",
Zip.Name,
ZipLength.TotalSeconds,
FinalZipInfo.Length / (1024.0f * 1024.0f)));
}
static public bool ExecuteCookCommand(string Command, string RPCCommand)
{
return false;
}
}
}