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

183 lines
5.7 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using AutomationTool;
using UnrealBuildBase;
using Microsoft.Extensions.Logging;
[Help(@"Creates an IPA from an xarchive file")]
[Help("method=<method>", @"Purpose of the IPA. (Development, Adhoc, Store)")]
[Help("TemplateFile=<file>", @"Path to plist template that will be filled in based on other arguments. See ExportOptions.plist.template for an example")]
[Help("OptionsFile", @"Path to an XML file of options that we'll select from based on method. See ExportOptions.Values.xml for an example")]
[Help("Project", @"Name of this project (e.g ShooterGame, EngineTest)")]
public class ExportIPAFromArchive : BuildCommand
{
public string TemplateFile { get; set; }
public string OptionsFile { get; set; }
public string Project { get; set; }
public string Method { get; set; }
public string Archive { get; set; }
public string Output { get; set; }
public ExportIPAFromArchive()
{
Method = "development";
}
string SelectPropertyTextOrDie(XmlDocument Doc, string InProperty, string InName)
{
string MethodSelect = string.Format("//{0}/type[@name=\"{1}\"]", InProperty, InName);
XmlNode MethodNode = Doc.SelectSingleNode(MethodSelect);
if (MethodNode == null)
{
throw new AutomationException("Failed to find node for property {0} with name {1}", InProperty, InName);
}
return MethodNode.InnerXml;
}
protected void CreateExportPlist(string OutputPath)
{
XmlDocument Doc = new XmlDocument();
Doc.XmlResolver = null;
Doc.Load(OptionsFile);
// select all entries via xpath
string MethodText = SelectPropertyTextOrDie(Doc, "methods", Method);
string SigningText = SelectPropertyTextOrDie(Doc, "signing", Method);
string ProfileText = SelectPropertyTextOrDie(Doc, "profiles", Method);
// replace them in the template
string TemplateContents = File.ReadAllText(TemplateFile);
TemplateContents = TemplateContents.Replace("{{method}}", MethodText);
TemplateContents = TemplateContents.Replace("{{profiles}}", ProfileText);
TemplateContents = TemplateContents.Replace("{{signing}}", SigningText);
// write it out
Directory.CreateDirectory(Path.GetDirectoryName(OutputPath));
File.WriteAllText(OutputPath, TemplateContents);
}
int XcodeBuild(string Arguments)
{
string XcodeBuildPath = "/usr/bin/xcodebuild";
IProcessResult Result = CommandUtils.Run(XcodeBuildPath, Arguments);
return Result.ExitCode;
}
public override ExitCode Execute()
{
Project = ParseParamValue("Project", Project);
OptionsFile = ParseParamValue("OptionsFile", OptionsFile);
TemplateFile = ParseParamValue("TemplateFile", TemplateFile);
Method = ParseParamValue("Method", Method);
Archive = ParseParamValue("archive", Archive);
Output = ParseParamValue("output", Output);
if (string.IsNullOrEmpty(Output))
{
Output = Path.Combine(Project, "Binaries", "IOS");
}
if (string.IsNullOrEmpty(Archive))
{
throw new AutomationException("No archive specified. Use -archive=<path>");
}
if (string.IsNullOrEmpty(OptionsFile))
{
throw new AutomationException("No OptionsFile specified. Use -OptionsFile=<path>");
}
if (string.IsNullOrEmpty(TemplateFile))
{
throw new AutomationException("No TemplateFile specified. Use -TemplateFile=<path>");
}
// assume the user could have passed in -output=Path or -output=Path/Foo.ipa
string OutputFile = Path.GetFileName(Output);
string OutputPath = Output;
if (!string.IsNullOrEmpty(OutputFile) && Path.GetExtension(OutputFile).Length > 0)
{
// they did pass in Foo.ipa (or Foo.something). so we have the file and
// can get the path.
OutputPath = Path.GetDirectoryName(Output);
}
else
{
// path was probably something like Binaries/IOS/Test so leave the output there and call
// the ipa after the archive name (which it may or may not be based on the product name in
// the archive
OutputFile = Path.ChangeExtension(Path.GetFileName(Archive), "ipa");
}
string PlistPath = string.Format(@"{0}/Intermediate/IOS/ExportIPA.plist", Project);
string ProvisioningArgs = "";
CreateExportPlist(PlistPath);
string ExportArgs = string.Format("-exportArchive -archivePath \"{0}\" -exportOptionsPlist {1} -exportPath \"{2}\" {3}", Archive, PlistPath, OutputPath, ProvisioningArgs);
Logger.LogInformation("Creating IPA at at {OutputPath} with {Method} profile.", OutputPath, Method);
DateTime ExportStartTime = DateTime.Now;
int Result = XcodeBuild(ExportArgs);
if (Result != 0)
{
Logger.LogError("XcodeBuild failed to export archive. {PlistPath} might help diagose the issue..", PlistPath);
return ExitCode.Error_FailedToCreateIPA;
}
// if the user supplied an actual output path (e.g. "Foo.ipa") then we probably have to rename the
// output as it will have the product name, which can be totally different than the archive name...
if (!string.IsNullOrEmpty(OutputFile))
{
// find the ipa that was just created...
DirectoryInfo Di = new DirectoryInfo(OutputPath);
FileInfo TmpFile = Di.GetFiles("*.ipa").Where(F => {
if (F.LastWriteTime >= ExportStartTime)
{
return true;
}
return false;
}).FirstOrDefault();
//string TmpFile = Path.ChangeExtension(Archive, "ipa");
string FinalFile = Path.Combine(Path.GetDirectoryName(TmpFile.FullName), OutputFile);
if (File.Exists(FinalFile))
{
File.Delete(FinalFile);
}
if (Directory.Exists(FinalFile))
{
throw new AutomationException("Output file {0} is a directory!", FinalFile);
}
TmpFile.MoveTo(FinalFile);
}
return ExitCode.Success;
}
}