remote_ios_build_3

en MacOS, Tutoriales, UE4, Windows

Compilación remota iOS usando UE4 (Parte 3)

Vamos a crear una herramienta para poder comprobar nuestro fichero mobileprovision antes de configurar nuestro entorno, un programa muy simple para comprobar que el fichero puede leerse correctamente y además mostrar también algunas de sus entradas.

Creando los archivos de aprovisinamiento
Configuración remota del proyecto para iOS
Mobileprovision tester

Formato del fichero .mobileprovision

El fichero .mobileprovision es utilizado por Xcode para crear aplicaciones iPhone, y contiene un perfil de aprovisionamiento, que permite a una app ser subida a un limitado número de iPhones o iPads cuando aún está en desarrollo.

Básicamente esta información está almacenada en formato XML, podemos abrir el fichero utilizando un editor de texto y ver la sección XML después de algo de información de la cabecera.

mobileprovision_header

Tendremos que extraer esta sección XML antes de ser capaces de leer cada una de las eqtiquetas del plist. Los datos de aprovisionamiento se almacenan en un fichero PKCS7-signed en formato ASN.1 BER por lo que necesitamos una clase para leer el formato ASN antes de implementar el lector de XML. Aqui tenemos una clase para procesar el formato ASN.1 BER:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace MobileProvisionReader
{
	/// <summary>
	/// Parser for ASN.1 BER formats (eg. iOS mobile provisions)
	/// </summary>
	static class Asn
	{
		public enum FieldTag
		{
			EOC = 0,
			BOOLEAN = 1,
			INTEGER = 2,
			OCTET_STRING = 4,
			NULL = 5,
			OBJECT_IDENTIFIER = 6,
			SEQUENCE = 16,
			SET = 17,
			PRINTABLE_STRING = 19,
		}

		public struct FieldInfo
		{
			public int TagClass;
			public bool bPrimitive;
			public FieldTag Tag;
			public int Length;
		}

		public static class ObjectIdentifier
		{
			public static readonly int[] CountryName = new int[] { 2, 5, 4, 6 };
			public static readonly int[] OrganizationName = new int[] { 2, 5, 4, 10 };
			public static readonly int[] Pkcs7_SignedData = new int[] { 1, 2, 840, 113549, 1, 7, 2 };
			public static readonly int[] Pkcs7_Data = new int[] { 1, 2, 840, 113549, 1, 7, 1 };
		}

		static readonly List<KeyValuePair<int[], string>> KnownObjectIdentifiers = new List<KeyValuePair<int[], string>>
		{
			new KeyValuePair<int[], string>(ObjectIdentifier.CountryName, "CountryName"),
			new KeyValuePair<int[], string>(ObjectIdentifier.OrganizationName, "OrganizationName"),
			new KeyValuePair<int[], string>(ObjectIdentifier.Pkcs7_SignedData, "Pkcs7-SignedData"),
			new KeyValuePair<int[], string>(ObjectIdentifier.Pkcs7_Data, "Pkcs7-Data"),
		};

		public static FieldInfo ReadField(BinaryReader Reader)
		{
			// Read the type and length
			int Type = Reader.ReadByte();
			int Length = ReadLength(Reader);

			// Unpack the type
			FieldInfo Field = new FieldInfo();
			Field.TagClass = (Type >> 6);
			Field.bPrimitive = (Type & 0x20) == 0;
			Field.Tag = (FieldTag)(Type & 0x1f);
			Field.Length = Length;
			return Field;
		}

		public static void SkipValue(BinaryReader Reader, FieldInfo Field)
		{
			if (Field.bPrimitive)
			{
				Reader.BaseStream.Seek(Field.Length, SeekOrigin.Current);
			}
		}

		public static int ReadInteger(BinaryReader Reader, int Length)
		{
			int Value = 0;
			for (int Idx = 0; Idx < Length; Idx++)
			{
				Value = (Value << 8) | (int)Reader.ReadByte();
			}
			return Value;
		}

		public static int ReadLength(BinaryReader Reader)
		{
			int Count = (int)Reader.ReadByte();
			if (Count <= 0x7f)
			{
				return Count;
			}
			else
			{
				return ReadInteger(Reader, Count & 0x7f);
			}
		}

		public static int[] ReadObjectIdentifier(BinaryReader Reader, int Length)
		{
			byte[] Data = Reader.ReadBytes(Length);

			List<int> Values = new List<int>();
			Values.Add((int)Data[0] / 40);
			Values.Add((int)Data[0] % 40);
			for (int Idx = 1; Idx < Data.Length; Idx++)
			{
				int Value = (int)Data[Idx] & 0x7f;
				while (((int)Data[Idx] & 0x80) != 0)
				{
					Value = (Value << 7) | ((int)Data[++Idx] & 0x7f);
				}
				Values.Add(Value);
			}
			return Values.ToArray();
		}

		public static string GetObjectIdentifierName(int[] Values)
		{
			string Description;
			if (!TryGetObjectIdentifierName(Values, out Description))
			{
				Description = String.Format("Unknown ({0})", String.Join(", ", Values.Select(x => x.ToString())));
			}
			return Description;
		}

		public static bool TryGetObjectIdentifierName(int[] Values, out string Name)
		{
			foreach (KeyValuePair<int[], string> KnownObjectIdentifier in KnownObjectIdentifiers)
			{
				if (Enumerable.SequenceEqual(Values, KnownObjectIdentifier.Key))
				{
					Name = KnownObjectIdentifier.Value;
					return true;
				}
			}

			Name = null;
			return false;
		}

		public static void Dump(BinaryReader Reader, string Indent)
		{
			FieldInfo Field = ReadField(Reader);

			// If it's a primitive type, unpack the value
			string DescriptionSuffix = "";
			if (Field.bPrimitive)
			{
				if (Field.Length > 0)
				{
					string Description;
					switch (Field.Tag)
					{
						case FieldTag.INTEGER:
							Description = String.Format("{0}", ReadInteger(Reader, Field.Length));
							break;
						case FieldTag.OBJECT_IDENTIFIER:
							Description = GetObjectIdentifierName(ReadObjectIdentifier(Reader, Field.Length));
							break;
						case FieldTag.PRINTABLE_STRING:
							Description = Encoding.ASCII.GetString(Reader.ReadBytes(Field.Length));
							break;
						default:
							Reader.ReadBytes(Field.Length);
							Description = "unknown";
							break;
					}
					DescriptionSuffix = String.Format(" = {0}", Description);
				}
			}

			// Print the contents/name of this element
			Console.WriteLine("{0}{1}{2}", Indent, Field.Tag, DescriptionSuffix);

			// If it's not a primitive, print the contents
			if (!Field.bPrimitive)
			{
				long EndOffset = Reader.BaseStream.Position + Field.Length;
				while (Reader.BaseStream.Position < EndOffset)
				{
					Dump(Reader, Indent + "  ");
				}
			}
		}
	}
}

Ahora podemos construir el extractor de XML apoyandonos en la clase anterior para procesar el fichero mobileprovision.

public static XmlDocument ReadXml(string Location)
{
	// Provision data is stored as PKCS7-signed file in ASN.1 BER format
	using (BinaryReader Reader = new BinaryReader(File.Open(Location, FileMode.Open, FileAccess.Read)))
	{
		long Length = Reader.BaseStream.Length;
		while (Reader.BaseStream.Position < Length)
		{
			Asn.FieldInfo Field = Asn.ReadField(Reader);
			if (Field.Tag == Asn.FieldTag.OBJECT_IDENTIFIER)
			{
				int[] Identifier = Asn.ReadObjectIdentifier(Reader, Field.Length);
				if (Enumerable.SequenceEqual(Identifier, Asn.ObjectIdentifier.Pkcs7_Data))
				{
					while (Reader.BaseStream.Position < Length)
					{
						Asn.FieldInfo NextField = Asn.ReadField(Reader);
						if (NextField.Tag == Asn.FieldTag.OCTET_STRING)
						{
							byte[] Data = Reader.ReadBytes(NextField.Length);

							XmlDocument Document = new XmlDocument();
							Document.Load(new MemoryStream(Data));
							return Document;
						}
						else
						{
							Asn.SkipValue(Reader, NextField);
						}
					}
				}
			}
			else
			{
				Asn.SkipValue(Reader, Field);
			}
		}
		throw new Exception("No PKCS7-Data section");
	}
}

Después de que la sección XML ha sido extraída podemos interactuar con esta utilizando las clases para XML de C#, pro ejemplo para devolver el valor de una clave específica, o para hacer un Diccionario con todos los pares clave-valor.

Dictionary<string, XmlElement> NameToValue = new Dictionary<string, XmlElement>();

foreach(XmlElement KeyElement in xmlDocument.SelectNodes("/plist/dict/key"))
{
	XmlNode ValueNode = KeyElement.NextSibling;
	while(ValueNode != null)
	{
		XmlElement ValueElement = ValueNode as XmlElement;
		if(ValueElement != null)
		{
			NameToValue[KeyElement.InnerText] = ValueElement;
			break;
		}
	}
}

Y utilizar este Diccionario después para consultar los valores de las claves.

XmlElement UniqueIdElement;
if(!NameToValue.TryGetValue("UUID", out UniqueIdElement))
{
	throw new BuildException("Missing UUID in MobileProvision");
}
return UniqueIdElement.InnerText;

Vamos a mostrar solo el identificador único, el de equipo y el de paquete, por lo tanto vamos a hacer un método devolver cada uno de ellos. Esta va a ser la última modificación de nuestra clase MobileProvisionContents:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;

namespace MobileProvisionReader
{
    class MobileProvisionContents
    {
		/// <summary>
		/// The contents of the provision
		/// </summary>
		XmlDocument Document;

		/// <summary>
		/// Map of key names to XML elements holding their values
		/// </summary>
		Dictionary<string, XmlElement> NameToValue = new Dictionary<string, XmlElement>();

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="Document">XML file to create the mobile provision from. Call Read() to read from a signed file on disk.</param>
		public MobileProvisionContents(XmlDocument Document)
		{
			this.Document = Document;

			foreach (XmlElement KeyElement in Document.SelectNodes("/plist/dict/key"))
			{
				XmlNode ValueNode = KeyElement.NextSibling;
				while (ValueNode != null)
				{
					XmlElement ValueElement = ValueNode as XmlElement;
					if (ValueElement != null)
					{
						NameToValue[KeyElement.InnerText] = ValueElement;
						break;
					}
				}
			}
		}

		/// <summary>
		/// Gets the unique id for this mobileprovision
		/// </summary>
		/// <returns>UUID for the provision</returns>
		public string GetUniqueId()
		{
			XmlElement UniqueIdElement;
			if (!NameToValue.TryGetValue("UUID", out UniqueIdElement))
			{
				throw new Exception("Missing UUID in MobileProvision");
			}
			return UniqueIdElement.InnerText;
		}

		/// <summary>
		/// Gets the bundle id for this mobileprovision
		/// </summary>
		/// <returns>Bundle Identifier for the provision</returns>
		public string GetBundleIdentifier()
		{
			XmlElement UniqueIdElement = null, UniqueIdEntitlement;
			if (!NameToValue.TryGetValue("Entitlements", out UniqueIdEntitlement) || UniqueIdEntitlement.Name != "dict")
			{
				throw new Exception("Missing Entitlements in MobileProvision");
			}

			foreach (XmlElement KeyElement in UniqueIdEntitlement.SelectNodes("key"))
			{
				Console.WriteLine("Found entitlement node:" + KeyElement.InnerText);
				if (!KeyElement.InnerText.Equals("application-identifier"))
				{
					continue;
				}
				UniqueIdElement = KeyElement.NextSibling as XmlElement;
				break;
			}


			if (UniqueIdElement == null)
			{
				throw new Exception("Missing Bundle Identifier in MobileProvision");
			}
			return UniqueIdElement.InnerText.Substring(UniqueIdElement.InnerText.IndexOf('.') + 1);
		}

		/// <summary>
		/// Gets the team unique id for this mobileprovision
		/// </summary>
		/// <param name="UniqueId">Receives the team unique id</param>
		/// <returns>True if the team unique ID was found, false otherwise</returns>
		public bool TryGetTeamUniqueId(out string UniqueId)
		{
			XmlElement UniqueIdElement;
			if (!NameToValue.TryGetValue("TeamIdentifier", out UniqueIdElement) || UniqueIdElement.Name != "array")
			{
				UniqueId = null;
				return false;
			}

			XmlElement ValueElement = UniqueIdElement.SelectSingleNode("string") as XmlElement;
			if (ValueElement == null)
			{
				UniqueId = null;
				return false;
			}

			UniqueId = ValueElement.InnerText;
			return true;
		}

		/// <summary>
		/// Reads a mobileprovision from a file on disk
		/// </summary>
		/// <param name="Location">Path to the file</param>
		/// <returns>New mobile provision instance</returns>
		public static MobileProvisionContents Read(string Location)
		{
			XmlDocument Document = ReadXml(Location);
			return new MobileProvisionContents(Document);
		}

		/// <summary>
		/// Reads the plist file inside a mobileprovision
		/// </summary>
		/// <param name="Location">Path to the file</param>
		/// <returns>XML plist extracted from the mobile provision</returns>
		public static XmlDocument ReadXml(string Location)
		{
			// Provision data is stored as PKCS7-signed file in ASN.1 BER format
			using (BinaryReader Reader = new BinaryReader(File.Open(Location, FileMode.Open, FileAccess.Read)))
			{
				long Length = Reader.BaseStream.Length;
				while (Reader.BaseStream.Position < Length)
				{
					Asn.FieldInfo Field = Asn.ReadField(Reader);
					if (Field.Tag == Asn.FieldTag.OBJECT_IDENTIFIER)
					{
						int[] Identifier = Asn.ReadObjectIdentifier(Reader, Field.Length);
						if (Enumerable.SequenceEqual(Identifier, Asn.ObjectIdentifier.Pkcs7_Data))
						{
							while (Reader.BaseStream.Position < Length)
							{
								Asn.FieldInfo NextField = Asn.ReadField(Reader);
								if (NextField.Tag == Asn.FieldTag.OCTET_STRING)
								{
									byte[] Data = Reader.ReadBytes(NextField.Length);

									XmlDocument Document = new XmlDocument();
									Document.Load(new MemoryStream(Data));
									return Document;
								}
								else
								{
									Asn.SkipValue(Reader, NextField);
								}
							}
						}
					}
					else
					{
						Asn.SkipValue(Reader, Field);
					}
				}
				throw new Exception("No PKCS7-Data section");
			}
		}
	}
}

En el método del punto de entrada del programa debemos hacer la llamada al método Read de esta clase para extraer los datos del XML y luego utilizar los métodos de acceso para cada uno de los valores que nos interesan.

static void Main(string[] args)
{
	try
	{
		//Read mobileprovision contents
		MobileProvisionContents mpc = MobileProvisionContents.Read("D://Selfsigned.mobileprovision");

		//Dispaly some mobileprovision entries
		string uuid = mpc.GetUniqueId();
		Console.WriteLine("UUID {0}", uuid);

		string teamUniqueId;
		if (mpc.TryGetTeamUniqueId(out teamUniqueId))
		{
			Console.WriteLine("TeamIdentifier: {0}", teamUniqueId);
		}
		else
		{
			Console.WriteLine("TeamIdentifier: missing");
		}

		string bundleid = mpc.GetBundleIdentifier();
		Console.WriteLine("BundleIdentifier: {0}", bundleid);

	}
	catch (Exception e)
	{
		Console.WriteLine("Reading ERROR Exception: {0}", e.Message);
	}
}

Si el programa es capaz de mostrar los valores que hemos seccionado entonces el mobileprovision es válido para compilar remotamente aplicaciones iOS utilizando el editor de UE4.

provisionreader_output

Si nos encontramos con una Exception el mobileprofile presenta un formato no válido, por lo que tendremos que volver a repetir los pasos de su generación nuevamente arreglando el problema antes, claro. Si se obtiene una excepción «Root element is missing» durante una compilación remota de UE4 o mientras usamos nuestro anterior programa reader program entonces eñ mobileprofile está corrupto.

Generating and uploading Crashlytics Data
ERROR: Unhandled exception: System.Xml.XmlException: Root element is missing.
	  at System.Xml.XmlTextReaderImpl.Throw (System.Exception e) [0x00027] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlTextReaderImpl.ThrowWithoutLineInfo (System.String res) [0x00017] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlTextReaderImpl.ParseDocumentContent () [0x0035d] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlTextReaderImpl.Read () [0x0008c] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlLoader.Load (System.Xml.XmlDocument doc, System.Xml.XmlReader reader, System.Boolean preserveWhitespace) [0x000a6] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlDocument.Load (System.Xml.XmlReader reader) [0x0002e] in <c615384cc92144aaa3f942d0c781696a>:0
	  at System.Xml.XmlDocument.Load (System.IO.Stream inStream) [0x00013] in <c615384cc92144aaa3f942d0c781696a>:0
	  at UnrealBuildTool.MobileProvisionContents.ReadXml (Tools.DotNETCommon.FileReference Location) [0x00080] in <4308e3bdfb1844339db4a61e5a2da573>:0
	  at UnrealBuildTool.MobileProvisionContents.Read (Tools.DotNETCommon.FileReference Location) [0x00000] in <4308e3bdfb1844339db4a61e5a2da573>:0
	  at UnrealBuildTool.IOSToolChain.PostBuildSync (UnrealBuildTool.IOSPostBuildSyncTarget Target) [0x00586] in <4308e3bdfb1844339db4a61e5a2da573>:0
	  at UnrealBuildTool.IOSPostBuildSyncMode.Execute (Tools.DotNETCommon.CommandLineArguments Arguments) [0x00018] in <4308e3bdfb1844339db4a61e5a2da573>:0
	  at UnrealBuildTool.UnrealBuildTool.Main (System.String[] ArgumentsArray) [0x002bb] in <4308e3bdfb1844339db4a61e5a2da573>:0
PackagingResults: Error: Unhandled exception: System.Xml.XmlException: Root element is missing.

Cuando se están compartiendo ficheros entre las plataformas de Windows y MacOS debemos tener en cuenta sus diferentes carácteres de fin de línea. Cuando creamos el fichero .plist tenemos que utilizarla terminacion LF, si utilizamos la CR/LF(Windows) crearemos un fichero mobileprovision no válido después.

line_termination_compare

Normalmente un buen editor de texto tendrá una herramienta para cambiar los carácteres de fin de línea, por lo que no nos hará falta cambiarlos en cada una de las líneas manualmente.

text_editor_termination_tool

Otra de las causas puede ser un XML con un error de síntaxis, por ejemplo una etiqueta sin cerrar, utilizar un validador de síntaxis xml lo puede arreglar rápidamente.

Y eso es todo, si encuentras algún otro problema deja un comentario y podemos buscar juntos una solución.

Downloads

Ayudanos con este blog!

El último año he estado dedicando cada vez más tiempo a la creación de tutoriales, en su mayoria sobre desarrollo de videojuegos. Si crees que estos posts te han ayudado de alguna manera o incluso inspirado, por favor considera ayudarnos a mantener este blog con alguna de estas opciones. Gracias por hacerlo posible!

Escribe un comentario

Comentario