+ [PostProcessBuild(2000)]
+ public static void OnPostProcessBuild(BuildTarget target, string path)
+ OnPostProcessBuild_iOS(target, path);
+ /// Class to represent *.projmods file which is used in XCodeEditor
+ /// Reference: https://github.com/dcariola/XCodeEditor-for-Unity
+ public class ProjectModsContent
+ /// The pluginpath in Unity project. Parent directory is [UnityProject
+ public string pluginpath;
+ /// The copy dependencies into xCode project
+ public bool copyDependencies;
+ /// The dependency file list to be added into xCode project
+ public List<string> dependencyList = new List<string> ();
+ /// all files and folders will be parented to this group
+ /// Gets the name of the projmod file.
+ /// <value>The name of the proj mod file.</value>
+ public string projModFileName
+ get { return group + "XCode.projmods"; }
+ /// Gets the framework search path for plugin.
+ /// <value>The framework search path for plugin.</value>
+ public string frameworkSearchPathForPlugin
+ return (copyDependencies ?
+ "$(SRCROOT)/" + group :
+ ConvertToMacPath (Path.Combine (Application.dataPath, PathWithPlatformDirSeparators (pluginpath))));
+ public List<string> patches = new List<string>();
+ /// add libraries to build phase
+ public List<string> libs = new List<string>();
+ public List<string> librarysearchpaths = new List<string>();
+ /// add frameworks to the project
+ public List<string> frameworks = new List<string>();
+ /// Add weak frameworks to the project
+ public List<string> weakframeworks = new List<string> ();
+ /// Subject to be udpated by calling UpdateThenCreateProjModFile.
+ /// frameworkSearchPathForPlugin will be added
+ public List<string> frameworksearchpaths = new List<string>();
+ /// add header paths to build phase
+ public List<string> headerpaths = new List<string>();
+ /// add single files to the project
+ /// Subject to be udpated by calling UpdateThenCreateProjModFile
+ public List<string> files = new List<string>();
+ /// create a subgroup and add all files to the project (recursive)
+ public List<string> folders = new List<string>();
+ /// file mask to exclude
+ public List<string> excludes = new List<string>();
+ /// The build setting single value.
+ public Dictionary<string, string> buildSettingSingleValue = new Dictionary<string, string> ();
+ /// The build setting multiple value.
+ // TODO: Not sure why we put JsonIgnore here
+ public Dictionary<string, List<string>> buildSettingMultipleValue = new Dictionary<string, List<string>>();
+ /// The development team ID. Used for setting System capabilities, since it's required to be set for CI
+ public string developmentTeamID = "";
+ /// The system capabilities value. To enable or disable project system capabilities like following, since
+ /// it's required to be set for CI since XCode 8
+ /// SystemCapabilities = {
+ /// com.apple.Wallet = {
+ public Dictionary<string, bool> systemCapabilitiesValue = new Dictionary<string, bool>();
+ /// From Everyplay's EveryplayPostprocessor.cs
+ public XmlNode ItemKeyNode;
+ public XmlNode ItemValueNode;
+ public PListItem(XmlNode keyNode, XmlNode valueNode)
+ ItemValueNode = valueNode;
+ public static void OnPostProcessBuild_iOS(BuildTarget target, string path)
+ // 1. Set projmods configuration
+ ProjectModsContent projmods = ProjectModsContent () {
+ // Values to update projmods
+ pluginpath = "Plugins/Ads",
+ copyDependencies = true,
+ dependencyList = new List<string> () {
+ "Chartboost.framework",
+ "GoogleMobileAds.framework",
+ patches = new List<string> (),
+ libs = new List<string> (),
+ librarysearchpaths = new List<string> (),
+ frameworks = new List<string> () {
+ "SafariServices.framework",
+ // NOTE BY DY: Can add more to modify projmod file of xCode
+ weakframeworks = new List<string> (),
+ frameworksearchpaths = new List<string> (),
+ headerpaths = new List<string> (),
+ files = new List<string> (),
+ folders = new List<string> (),
+ excludes = new List<string> (),
+ buildSettingSingleValue = new Dictionary<string, string>()
+ { "ENABLE_BITCODE", "NO" },
+ * Required for OneSignal & UnityCloudBuild
+ * Since xCode8, Build like UnityCloudBuild doesn't enable Push notification base on Provisioning's capability.
+ * Thus, we got to modify projmod, plist and entitlement.
+ * More detail of the issue is following, https://forum.unity3d.com/threads/ios-capabilities.380740/#post-2916100
+ #region Required for OneSignal & UnityCloudBuild
+ // Necessary to enable Push notification in iOS with building in CI like UnityClouldBuild.
+ // It will also trigger to create .entitlements file which is XML format
+ { "CODE_SIGN_ENTITLEMENTS", "Unity-iPhone/ios.entitlements" }
+ // NOTE BY DY: Can add more to modify projmod file of xCode
+ //buildSettingMultipleValue = new Dictionary<string, List<string>>();
+ #region Required for OneSignal & UnityCloudBuild
+ // Can find from the certification of distribution team between ( and )
+ developmentTeamID = "YOUR_TEAM_UDID_IN_CERTIFICATION",
+ systemCapabilitiesValue = new Dictionary<string, bool>()
+ { "com.apple.Push", true },
+ // Required by OneSignal. Need to modifiy plist, too
+ { "com.apple.BackgroundModes", true }
+ // 2. Update and Apply ProjectModsContent
+ UpdateProjModFileThenApply (path, projmods);
+ new Dictionary<string, List<string>> () {
+ { "NSCalendarsUsageDescription", new List<string>() { "Some Ad contents may access calendars" } },
+ { "NSPhotoLibraryUsageDescription", new List<string>() { "Customer service may access photos" } },
+ #region Required for OneSignal & UnityCloudBuild
+ // Required by OneSignal
+ // NOTE BY DY: Empty string element is required to add string array with only one element
+ { "UIBackgroundModes", new List<string>() { "remote-notification", "" } },
+ //{ "CFBundleURLTypes", new List<string>() { "Only for test" } },
+ // NOTE BY DY: Example of List<KeyValuePair<string, List<string>>> urlNameAndSchemeArrayPairList = null
+ ,new List<KeyValuePair<string, List<string>>>()
+ new KeyValuePair<string, List<string>>(
+ new KeyValuePair<string, List<string>>(
+ new KeyValuePair<string, List<string>>(
+ new KeyValuePair<string, List<string>>(
+ /// Update given ProjectModsContent.folders and ProjectModsContent.frameworksearchpaths, then apply to xCode project
+ /// <param name="path">Path.</param>
+ /// <param name="projMod">Proj mod.</param>
+ public static void UpdateProjModFileThenApply(string path, ProjectModsContent projMod)
+ string modPath = Path.Combine (path, projMod.group);
+ if (Directory.Exists (modPath))
+ FileUtility.ClearDirectory (modPath, false);
+ Directory.CreateDirectory (modPath);
+ string pluginsPath = Path.Combine (Application.dataPath, PathWithPlatformDirSeparators (projMod.pluginpath));
+ projMod.folders.Clear (); // Will be updated depending on projMod.copydependencies
+ #region Modify pbxproj file
+ string projectPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
+ // NOTE BY DY: Got from MoPubInternal.Editor.ThirdParty.xcodeapi.PBXProject
+ PBXProject pbxProject = new PBXProject ();
+ pbxProject.ReadFromFile (projectPath);
+ // NOTE BY DY: Since this is for Unity project, main target named is pretty much fixed as "Unity-iPhone"
+ string targetGUID = pbxProject.TargetGuidByName ("Unity-iPhone");
+ string dependencyTargetPath = projMod.copyDependencies ? modPath : pluginsPath;
+ string targetFile, source;
+ foreach (string dependencyFile in projMod.dependencyList)
+ targetFile = Path.Combine (dependencyTargetPath, dependencyFile);
+ source = Path.Combine (pluginsPath, dependencyFile);
+ if (projMod.copyDependencies)
+ if (Directory.Exists (source))
+ FileUtility.DirectoryCopy (source, targetFile);
+ else if (File.Exists (source))
+ File.Copy (source, targetFile);
+ targetFile = Path.Combine(projMod.group, dependencyFile);
+ Debug.Log ("Copy & Add file - " + targetFile);
+ pbxProject.AddFileToBuild(targetGUID,
+ // NOTE BY DY: Got from MoPubInternal.Editor.ThirdParty.xcodeapi.PBXSourceTree
+ pbxProject.AddFile ( targetFile, targetFile, PBXSourceTree.Source));
+ } catch (System.Exception e)
+ Debug.Log ("Unable to copy file or directory, " + e.Message);
+ targetFile = Path.Combine(projMod.group, dependencyFile);
+ Debug.Log ("Copy & Add file - s: " + source + " t: " + targetFile);
+ pbxProject.AddFileToBuild(targetGUID,
+ pbxProject.AddFile (source, targetFile, PBXSourceTree.Absolute));
+ projMod.files.Add (FileUtility.ConvertToMacPath (targetFile));
+ if (!projMod.frameworksearchpaths.Contains (projMod.frameworkSearchPathForPlugin))
+ projMod.frameworksearchpaths.Add (projMod.frameworkSearchPathForPlugin);
+ foreach (string framework in projMod.frameworks) {
+ Debug.Log("Add framework - " + framework);
+ pbxProject.AddFrameworkToProject (targetGUID, framework, false);
+ foreach (string weakframework in projMod.weakframeworks) {
+ Debug.Log("Add weakframework - " + weakframework);
+ pbxProject.AddFrameworkToProject (targetGUID, weakframework, true);
+ foreach (string frameworkSearchPath in projMod.frameworksearchpaths) {
+ Debug.Log("Add framework search path - " + frameworkSearchPath);
+ pbxProject.AddBuildProperty (targetGUID, "FRAMEWORK_SEARCH_PATHS", string.Format ("\"{0}\"", frameworkSearchPath));
+ foreach (string librarySearchPath in projMod.librarysearchpaths) {
+ Debug.Log("Add framework search path - " + librarySearchPath);
+ pbxProject.AddBuildProperty (targetGUID, "LIBRARY_SEARCH_PATHS", string.Format ("\"{0}\"", librarySearchPath));
+ foreach (string headerPath in projMod.headerpaths) {
+ Debug.Log("Add header path - " + headerPath);
+ pbxProject.AddBuildProperty (targetGUID, "HEADER_SEARCH_PATHS", string.Format ("\"{0}\"", headerPath));
+ foreach (KeyValuePair<string, string> pair in projMod.buildSettingSingleValue) {
+ Debug.Log ("Set property - n: " + pair.Key + " v: " + pair.Value);
+ pbxProject.SetBuildProperty (targetGUID, pair.Key, pair.Value);
+ foreach (KeyValuePair<string, List<string>> pair in projMod.buildSettingMultipleValue) {
+ if (pair.Value == null || pair.Value.Count == 0) continue;
+ Debug.Log ("Set property - n: " + pair.Key + " v: " + pair.Value [0]);
+ pbxProject.SetBuildProperty (targetGUID, pair.Key, pair.Value [0]);
+ for (int i = 1; i < pair.Value.Count; i++)
+ Debug.Log ("Add property - n: " + pair.Key + " v: " + pair.Value [i]);
+ pbxProject.AddBuildProperty (targetGUID, pair.Key, pair.Value [i]);
+ #region Setting system capabilities in pbxproj file
+ * NOTE BY DY: Couldn't figure out how to modify PBXProject section with PBXProject class.
+ * So, premitively, export currenlty modified PBXProject, and then insert System capabliity relatives,
+ * then import the string to PBXProject, again.
+ * Origined from: https://teratail.com/questions/52234
+ if (projMod.systemCapabilitiesValue.Count > 0 && !string.IsNullOrEmpty (projMod.developmentTeamID)) {
+ string[] lines = pbxProject.WriteToString ().Split ('\n');
+ List<string> newLines = new List<string> ();
+ bool editFinish = false;
+ for (int i = 0; i < lines.Length; i++) {
+ else if (line.IndexOf ("isa = PBXProject;") > -1) {
+ while (line.IndexOf ("TargetAttributes = {") == -1) {
+ newLines.Add (line); // Add "TargetAttributes = {"
+ newLines.Add (pbxProject.TargetGuidByName ("Unity-iPhone") + " = {");
+ newLines.Add (string.Format ("DevelopmentTeam = {0};", projMod.developmentTeamID));
+ newLines.Add ("SystemCapabilities = {");
+ foreach (KeyValuePair<string, bool> capability in projMod.systemCapabilitiesValue) {
+ Debug.Log ("System capability, " + capability.Key + " , enable: " + capability.Value);
+ newLines.Add (capability.Key + " = {");
+ newLines.Add (string.Format ("enabled = {0};", (int)(capability.Value ? 1 : 0)));
+ pbxProject.ReadFromString (string.Join ("\n", newLines.ToArray ()));
+ if (projMod.systemCapabilitiesValue.Count > 0 && string.IsNullOrEmpty (projMod.developmentTeamID))
+ Debug.LogWarning ("Must enter DevelopmentTeamID to enable or disable SystemCapabilities");
+ #endregion Setting system capabilities in pbxproj file
+ Debug.Log ("Finish to update pbxproj at " + projectPath);
+ pbxProject.WriteToFile (projectPath);
+ #endregion Modify pbxproj file
+ // Required for OneSignal & UnityCloudBuild
+ #region Create or modify .entitlement
+ * Sample .entitlement>>
+ * <?xml version="1.0" encoding="UTF-8"?>
+ * <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+ * <plist version="1.0">
+ * <key>aps-environment</key>
+ * <string>development</string>
+ if (projMod.buildSettingSingleValue.ContainsKey ("CODE_SIGN_ENTITLEMENTS")) {
+ string filePath = Path.Combine (path, projMod.buildSettingSingleValue["CODE_SIGN_ENTITLEMENTS"]);
+ XmlDocument xmlDoc = new XmlDocument ();
+ if (File.Exists (filePath)) {
+ xmlDoc.Load (filePath);
+ dict = xmlDoc.SelectSingleNode ("plist/dict");
+ XmlDeclaration declaration = xmlDoc.CreateXmlDeclaration ("1.0", "UTF-8", null);
+ xmlDoc.InsertBefore (declaration, xmlDoc.DocumentElement);
+ XmlElement plist = xmlDoc.CreateElement ("plist");
+ XmlAttribute attr = xmlDoc.CreateAttribute ("version");
+ plist.Attributes.Append (attr);
+ xmlDoc.AppendChild (plist);
+ dict = plist.AppendChild (xmlDoc.CreateNode (XmlNodeType.Element, "dict", ""));
+ PListItem item = GetPlistItem (dict, "aps-environment");
+ XmlElement key = xmlDoc.CreateElement ("key");
+ key.InnerText = "aps-environment";
+ dict.AppendChild (key);
+ XmlElement str = xmlDoc.CreateElement ("string");
+ str.InnerText = "development";
+ Debug.Log ("Finished to generate entitlement file at the path: " + filePath);
+ xmlDoc.Save (filePath);
+ // Remove extra garbage added by the XmlDocument save
+ FileUtility.UpdateStringInFile (filePath, "dtd\"[]>", "dtd\">");
+ #endregion Create or modify .entitlement
+ /// Update xCode Info.plist with given keyValuePairs
+ /// Example of KeyValuePairs
+ /// new Dictionary<string, string> () {
+ /// { "NSCalendarsUsageDescription", "Some Ad contents may access calendars" },
+ /// { "NSPhotoLibraryUsageDescription", "Customer service may access photos" },
+ /// Example of urlNameAndSchemeArrayPairList
+ /// new List<KeyValuePair<string, List<string>>>() {
+ /// new KeyValuePair<string, List<string>>(
+ /// new KeyValuePair<string, List<string>>(
+ /// "testUrlScheme001",
+ /// "testUrlScheme002",
+ /// new KeyValuePair<string, List<string>>(
+ /// "testUrlScheme003",
+ /// "testUrlScheme004",
+ /// <param name="path">Path.</param>
+ /// <param name="keysAndValues">Keys and values.</param>
+ public static void UpdateXCodeInfoPList(
+ Dictionary<string, List<string>> keysAndValues,
+ List<KeyValuePair<string, List<string>>> urlNameAndSchemeArrayPairList = null)
+ if (keysAndValues == null || keysAndValues.Count == 0)
+ // Fix if there are problems on the given path
+ path = FixProblemInPathToBuildProject (path);
+ string filePath = Path.Combine (path, "Info.plist");
+ if (!File.Exists (filePath)) {
+ // NOTE BY DY: Got from System.Xml
+ XmlDocument xmlDoc = new XmlDocument ();
+ xmlDoc.Load (filePath);
+ XmlNode dict = xmlDoc.SelectSingleNode ("plist/dict");
+ foreach (KeyValuePair<string, List<string>> pair in keysAndValues) {
+ item = GetPlistItem (dict, pair.Key);
+ if(pair.Key.Equals("CFBundleURLTypes"))
+ if(urlNameAndSchemeArrayPairList != null && urlNameAndSchemeArrayPairList.Count >0)
+ XmlElement key = xmlDoc.CreateElement("key");
+ key.InnerText = "CFBundleURLTypes";
+ XmlElement array = xmlDoc.CreateElement("array");
+ item = new PListItem(dict.AppendChild(key), dict.AppendChild(array));
+ for(int i = 0; i < urlNameAndSchemeArrayPairList.Count; i++)
+ AddUrlScheme(xmlDoc, item.ItemValueNode, urlNameAndSchemeArrayPairList[i].Key, urlNameAndSchemeArrayPairList[i].Value);
+ // Normal element types
+ if(pair.Value.Count > 0)
+ XmlElement key = xmlDoc.CreateElement ("key");
+ key.InnerText = pair.Key;
+ dict.AppendChild (key);
+ // Append string typed Xml element
+ if(pair.Value.Count == 1)
+ XmlElement str = xmlDoc.CreateElement ("string");
+ str.InnerText = pair.Value[0];
+ // Append string array typed Xml element
+ XmlElement array = xmlDoc.CreateElement("array");
+ for(int i = 0; i < pair.Value.Count; i++)
+ if(string.IsNullOrEmpty(pair.Value[i])) continue;
+ str = xmlDoc.CreateElement("string");
+ str.InnerText = pair.Value[i];
+ array.AppendChild(str);
+ dict.AppendChild (array);
+ Debug.LogWarning("Don't have values for Key: " + pair.Key);
+ else // overwrite value
+ if(pair.Value.Count > 0)
+ if(item.ItemValueNode.Name.ToLower().Equals("string"))
+ item.ItemValueNode.InnerText = pair.Value[0];
+ else if(item.ItemValueNode.Name.ToLower().Equals("array"))
+ item.ItemValueNode.RemoveAll();
+ for(int i = 0; i < pair.Value.Count; i++)
+ if(string.IsNullOrEmpty(pair.Value[i])) continue;
+ str = xmlDoc.CreateElement("string");
+ str.InnerText = pair.Value[i];
+ item.ItemValueNode.AppendChild(str);
+ Debug.LogWarning("Don't have values for Key: " + pair.Key);
+ xmlDoc.Save (filePath);
+ // Remove extra garbage added by the XmlDocument save
+ UpdateStringInFile (filePath, "dtd\"[]>", "dtd\">");
+ Debug.Log ("Info.plist is not valid");
+ } catch (System.Exception e) {
+ Debug.Log ("Unable to update Info.plist: " + e.Message);
+ /// From Everyplay's EveryplayPostprocessor.cs
+ /// <param name="xmlDoc">Xml document.</param>
+ /// <param name="dictContainer">Dict container.</param>
+ /// <param name="urlScheme">URL scheme.</param>
+ private static void AddUrlScheme(XmlDocument xmlDoc, XmlNode dictContainer, string urlName, List<string> urlSchemes)
+ // If UrlName is required to be added
+ if (!string.IsNullOrEmpty (urlName)) {
+ if (!CheckIfUrlNameExists (dictContainer, urlName)) {
+ // If it doesn't exist, add URLName
+ dict = xmlDoc.CreateElement ("dict");
+ XmlElement key = xmlDoc.CreateElement ("key");
+ key.InnerText = "CFBundleURLName";
+ XmlElement str = xmlDoc.CreateElement ("string");
+ str.InnerText = urlName;
+ dict.AppendChild (key);
+ dict.AppendChild (str);
+ dictContainer.AppendChild (dict);
+ // If it existed, get dictionary contains the UrlName
+ dict = FindDictionaryForUrlName (dictContainer, urlName);
+ // If UrlSchemes are required to be added
+ if (urlSchemes != null && urlSchemes.Count > 0)
+ dict = xmlDoc.CreateElement ("dict");
+ dictContainer.AppendChild (dict);
+ PListItem bundleUrlSchemes = GetPlistItem(dict, "CFBundleURLSchemes");
+ if (bundleUrlSchemes == null) {
+ XmlElement key = xmlDoc.CreateElement ("key");
+ key.InnerText = "CFBundleURLSchemes";
+ array = xmlDoc.CreateElement ("array");
+ dict.AppendChild (key);
+ dict.AppendChild (array);
+ // If the array has existed,remove all children
+ array = bundleUrlSchemes.ItemValueNode;
+ for (int i = 0; i < urlSchemes.Count; i++)
+ XmlElement str = xmlDoc.CreateElement ("string");
+ str.InnerText = urlSchemes [i];
+ array.AppendChild (str);
+ /// Convert given path string to Mac path
+ /// <returns>The to mac path.</returns>
+ /// <param name="path">Path.</param>
+ public static string ConvertToMacPath(string path)
+ return path.Replace (@"\", "/");
+ /// Paths the with platform dir separators.
+ /// <returns>The with platform dir separators.</returns>
+ /// <param name="path">Path.</param>
+ public static string PathWithPlatformDirSeparators(string path)
+ if (Path.DirectorySeparatorChar == '/')
+ return path.Replace("\\", Path.DirectorySeparatorChar.ToString());
+ else if (Path.DirectorySeparatorChar == '\\')
+ return path.Replace("/", Path.DirectorySeparatorChar.ToString());
+ /// Clears the directory.
+ /// <param name="path">Path.</param>
+ /// <param name="deleteParent">If set to <c>true</c> delete parent.</param>
+ public static void ClearDirectory(string path, bool deleteParent)
+ string[] folders = Directory.GetDirectories(path);
+ foreach (string folder in folders)
+ ClearDirectory(folder, true);
+ string[] files = Directory.GetFiles(path);
+ foreach (string file in files)
+ Directory.Delete(path);
+ public static string FixProblemInPathToBuildProject(string pathToExam)
+ if (pathToExam.StartsWith ("./")) // Fix two erroneous path cases on Unity 5.4.f03
+ pathToExam = Path.Combine (Application.dataPath.Replace ("Assets", ""), pathToExam.Replace ("./", ""));
+ else if (pathToExam.Contains ("./"))
+ pathToExam = pathToExam.Replace ("./", "");
+ /// Replaced every occurance of string subject to string replacement
+ /// This method will delete the file with the filePath once, then create after replacing strings
+ /// From Everyplay's EveryplayPostprocessor.cs
+ /// <param name="filePath">File path.</param>
+ /// <param name="subject">Subject to be replaced</param>
+ /// <param name="replacement">Replacement</param>
+ public static void UpdateStringInFile(string filePath, string subject, string replacement)
+ if (!File.Exists(filePath))
+ string processedContents = "";
+ using (StreamReader sr = new StreamReader(filePath))
+ string line = sr.ReadLine();
+ processedContents += line.Replace(subject, replacement) + "\n";
+ using (StreamWriter streamWriter = File.CreateText(filePath))
+ streamWriter.Write(processedContents);
+ catch (System.Exception e)
+ Debug.Log("Unable to update string in file: " + e);