diff --git a/Assets/Appirater.meta b/Assets/Appirater.meta new file mode 100644 index 0000000..7d92cd3 --- /dev/null +++ b/Assets/Appirater.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: c9b125bd38ef74c00b6bd280c70f2694 +folderAsset: yes +timeCreated: 1511992838 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/Appirater.cs b/Assets/Appirater/Appirater.cs new file mode 100644 index 0000000..d233fd2 --- /dev/null +++ b/Assets/Appirater/Appirater.cs @@ -0,0 +1,38 @@ +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Appirater +{ + public class Appirater + { + + [DllImport("__Internal")] + private static extern void _Init(string appId, int daysUntilPrompt, int usesUntilPrompt, + int significantEventsUntilPrompt, int timeBeforeReminding, bool debug); + + + public static void Init(string appId, int daysUntilPrompt, int usesUntilPrompt, + int significantEventsUntilPrompt, int timeBeforeReminding, bool debug) + { + _Init(appId, daysUntilPrompt, usesUntilPrompt, significantEventsUntilPrompt, timeBeforeReminding, debug); + } + + [DllImport("__Internal")] + private static extern void _DidSignificantEvent(); + + public static void DidSignificantEvent() + { + _DidSignificantEvent(); + } + + [DllImport("__Internal")] + private static extern void _AppGoesToBackground(bool idInBackground); + + public void AppGoesToBackground(bool isInBackground) + { + AppGoesToBackground(isInBackground); + } + } +} diff --git a/Assets/Appirater/Appirater.cs.meta b/Assets/Appirater/Appirater.cs.meta new file mode 100644 index 0000000..2812759 --- /dev/null +++ b/Assets/Appirater/Appirater.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: eb4f3539f53ad4967915527e2baca684 +timeCreated: 1511996038 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale.meta b/Assets/Appirater/NativeLocale.meta new file mode 100644 index 0000000..3634ec5 --- /dev/null +++ b/Assets/Appirater/NativeLocale.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 1a71c0dae03cd4eb0ae4eb738a43506b +folderAsset: yes +timeCreated: 1511994683 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor.meta b/Assets/Appirater/NativeLocale/Editor.meta new file mode 100644 index 0000000..f05f5ff --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 40ea972b444d54436a89e35bd5a77704 +folderAsset: yes +timeCreated: 1496823339 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/NativeLocale.cs b/Assets/Appirater/NativeLocale/Editor/NativeLocale.cs new file mode 100644 index 0000000..a3af0de --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/NativeLocale.cs @@ -0,0 +1,80 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using System.IO; +using ChillyRoom.UnityEditor.iOS.Xcode; + +public class NativeLocale +{ + public static void AddLocalizedStringsIOS(string projectPath, string localizedDirectoryPath) + { + DirectoryInfo dir = new DirectoryInfo(localizedDirectoryPath); + if(!dir.Exists) + return; + + List locales = new List(); + var localeDirs = dir.GetDirectories("*.lproj", SearchOption.TopDirectoryOnly); + + foreach(var sub in localeDirs) + locales.Add(Path.GetFileNameWithoutExtension(sub.Name)); + + AddLocalizedStringsIOS(projectPath, localizedDirectoryPath, locales); + } + + public static void AddLocalizedStringsIOS(string projectPath, string localizedDirectoryPath, List validLocales) + { + string projPath = projectPath + "/Unity-iPhone.xcodeproj/project.pbxproj"; + PBXProject proj = new PBXProject(); + proj.ReadFromFile(projPath); + + foreach(var locale in validLocales) + { + // copy contents in the localization directory to project directory + string src = Path.Combine(localizedDirectoryPath, locale + ".lproj"); + string dst = Path.Combine(projectPath, "Unity-iPhone/" + locale + ".lproj"); + if (Directory.Exists(dst)) + { + Directory.Delete(dst,true); + } + DirectoryCopy(src, dst); + + string fileRelatvePath = string.Format("Unity-iPhone/{0}.lproj/AppiraterLocalizable.strings", locale); + proj.AddLocalization("InfoPlist.strings", locale, fileRelatvePath); + } + + proj.WriteToFile(projPath); + } + + + private static void DirectoryCopy(string sourceDirName, string destDirName) + { + DirectoryInfo dir = new DirectoryInfo(sourceDirName); + + if (!dir.Exists) + return; + + if (!Directory.Exists(destDirName)) + { + Directory.CreateDirectory(destDirName); + } + + FileInfo[] files = dir.GetFiles(); + + foreach (FileInfo file in files) + { + // skip unity meta files + if(file.FullName.EndsWith(".meta")) + continue; + string temppath = Path.Combine(destDirName, file.Name); + file.CopyTo(temppath, false); + } + + DirectoryInfo[] dirs = dir.GetDirectories(); + foreach (DirectoryInfo subdir in dirs) + { + string temppath = Path.Combine(destDirName, subdir.Name); + DirectoryCopy(subdir.FullName, temppath); + } + } +} diff --git a/Assets/Appirater/NativeLocale/Editor/NativeLocale.cs.meta b/Assets/Appirater/NativeLocale/Editor/NativeLocale.cs.meta new file mode 100644 index 0000000..d4b4d44 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/NativeLocale.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 3a34fc5c202884be2881f0ddb1d611d7 +timeCreated: 1496821555 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/PostProcess.cs b/Assets/Appirater/NativeLocale/Editor/PostProcess.cs new file mode 100644 index 0000000..4fe5392 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/PostProcess.cs @@ -0,0 +1,51 @@ +using UnityEngine; +using UnityEditor; +using UnityEditor.Callbacks; +using System.IO; +using ChillyRoom.UnityEditor.iOS.Xcode; + +public class PostProcess: MonoBehaviour +{ + [PostProcessBuildAttribute(9999)] + public static void OnPostProcessBuild(BuildTarget target, string path) + { + if (target == BuildTarget.iOS) + { + OnIOSBuild(target, path); + } + } + + private static void OnIOSBuild(BuildTarget target, string path) + { + NativeLocale.AddLocalizedStringsIOS(path, Path.Combine(Application.dataPath, "Appirater/iOS")); + AddStoreKitFramework(path); + } + + private const string SoreKitFramework = "StoreKit.framework"; + private static void AddStoreKitFramework(string projectPath) + { + + string projPath = projectPath + "/Unity-iPhone.xcodeproj/project.pbxproj"; + PBXProject proj = new PBXProject(); + proj.ReadFromFile(projPath); + +// if (!proj.ContainsFramework(proj.ProjectGuid(), SoreKitFramework)) +// { +// proj.AddFrameworkToProject(proj.ProjectGuid(), SoreKitFramework, false); +// } + + + foreach (string targetName in new string[] {PBXProject.GetUnityTargetName(), PBXProject.GetUnityTestTargetName()}) + { + string guid = proj.TargetGuidByName(targetName); + if (!proj.ContainsFramework(guid, SoreKitFramework)) + { + proj.AddFrameworkToProject(guid, SoreKitFramework, false); + } + } + + + + proj.WriteToFile(projPath); + } +} diff --git a/Assets/Appirater/NativeLocale/Editor/PostProcess.cs.meta b/Assets/Appirater/NativeLocale/Editor/PostProcess.cs.meta new file mode 100644 index 0000000..fa6c961 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/PostProcess.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 777d9d98c8cc74401891e2401fe521ea +timeCreated: 1496809301 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode.meta b/Assets/Appirater/NativeLocale/Editor/Xcode.meta new file mode 100644 index 0000000..b872cfc --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: bef53ee1a693a4d14adb39b28f71e82f +folderAsset: yes +timeCreated: 1496809275 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/AssetCatalog.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/AssetCatalog.cs new file mode 100644 index 0000000..588758d --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/AssetCatalog.cs @@ -0,0 +1,813 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ChillyRoom.UnityEditor.iOS.Xcode +{ + internal class DeviceTypeRequirement + { + public static readonly string Key = "idiom"; + public static readonly string Any = "universal"; + public static readonly string iPhone = "iphone"; + public static readonly string iPad = "ipad"; + public static readonly string Mac = "mac"; + public static readonly string iWatch = "watch"; + } + + internal class MemoryRequirement + { + public static readonly string Key = "memory"; + public static readonly string Any = ""; + public static readonly string Mem1GB = "1GB"; + public static readonly string Mem2GB = "2GB"; + } + + internal class GraphicsRequirement + { + public static readonly string Key = "graphics-feature-set"; + public static readonly string Any = ""; + public static readonly string Metal1v2 = "metal1v2"; + public static readonly string Metal2v2 = "metal2v2"; + } + + // only used for image sets + internal class SizeClassRequirement + { + public static readonly string HeightKey = "height-class"; + public static readonly string WidthKey = "width-class"; + public static readonly string Any = ""; + public static readonly string Compact = "compact"; + public static readonly string Regular = "regular"; + } + + // only used for image sets + internal class ScaleRequirement + { + public static readonly string Key = "scale"; + public static readonly string Any = ""; // vector image + public static readonly string X1 = "1x"; + public static readonly string X2 = "2x"; + public static readonly string X3 = "3x"; + } + + internal class DeviceRequirement + { + internal Dictionary values = new Dictionary(); + + public DeviceRequirement AddDevice(string device) + { + AddCustom(DeviceTypeRequirement.Key, device); + return this; + } + + public DeviceRequirement AddMemory(string memory) + { + AddCustom(MemoryRequirement.Key, memory); + return this; + } + + public DeviceRequirement AddGraphics(string graphics) + { + AddCustom(GraphicsRequirement.Key, graphics); + return this; + } + + public DeviceRequirement AddWidthClass(string sizeClass) + { + AddCustom(SizeClassRequirement.WidthKey, sizeClass); + return this; + } + + public DeviceRequirement AddHeightClass(string sizeClass) + { + AddCustom(SizeClassRequirement.HeightKey, sizeClass); + return this; + } + + public DeviceRequirement AddScale(string scale) + { + AddCustom(ScaleRequirement.Key, scale); + return this; + } + + public DeviceRequirement AddCustom(string key, string value) + { + if (values.ContainsKey(key)) + values.Remove(key); + values.Add(key, value); + return this; + } + + public DeviceRequirement() + { + values.Add("idiom", DeviceTypeRequirement.Any); + } + } + + internal class AssetCatalog + { + AssetFolder m_Root; + + public string path { get { return m_Root.path; } } + public AssetFolder root { get { return m_Root; } } + + public AssetCatalog(string path, string authorId) + { + if (Path.GetExtension(path) != ".xcassets") + throw new Exception("Asset catalogs must have xcassets extension"); + m_Root = new AssetFolder(path, null, authorId); + } + + AssetFolder OpenFolderForResource(string relativePath) + { + var pathItems = PBXPath.Split(relativePath).ToList(); + + // remove path filename + pathItems.RemoveAt(pathItems.Count - 1); + + AssetFolder folder = root; + foreach (var pathItem in pathItems) + folder = folder.OpenFolder(pathItem); + return folder; + } + + // Checks if a dataset at the given path exists and returns it if it does. + // Otherwise, creates a new dataset. Parent folders are created if needed. + // Note: the path is filesystem path, not logical asset name formed + // only from names of the folders that have "provides namespace" attribute. + // If you want to put certain resources in folders with namespace, first + // manually create the folders and then set the providesNamespace attribute. + // OpenNamespacedFolder may help to do this. + public AssetDataSet OpenDataSet(string relativePath) + { + var folder = OpenFolderForResource(relativePath); + return folder.OpenDataSet(Path.GetFileName(relativePath)); + } + + public AssetImageSet OpenImageSet(string relativePath) + { + var folder = OpenFolderForResource(relativePath); + return folder.OpenImageSet(Path.GetFileName(relativePath)); + } + + public AssetImageStack OpenImageStack(string relativePath) + { + var folder = OpenFolderForResource(relativePath); + return folder.OpenImageStack(Path.GetFileName(relativePath)); + } + + public AssetBrandAssetGroup OpenBrandAssetGroup(string relativePath) + { + var folder = OpenFolderForResource(relativePath); + return folder.OpenBrandAssetGroup(Path.GetFileName(relativePath)); + } + + // Checks if a folder with given path exists and returns it if it does. + // Otherwise, creates a new folder. Parent folders are created if needed. + public AssetFolder OpenFolder(string relativePath) + { + if (relativePath == null) + return root; + var pathItems = PBXPath.Split(relativePath); + if (pathItems.Length == 0) + return root; + AssetFolder folder = root; + foreach (var pathItem in pathItems) + folder = folder.OpenFolder(pathItem); + return folder; + } + + // Creates a directory structure with "provides namespace" attribute. + // First, retrieves or creates the directory at relativeBasePath, creating parent + // directories if needed. Effectively calls OpenFolder(relativeBasePath). + // Then, relative to this directory, creates namespacePath directories with "provides + // namespace" attribute set. Fails if the attribute can't be set. + public AssetFolder OpenNamespacedFolder(string relativeBasePath, string namespacePath) + { + var folder = OpenFolder(relativeBasePath); + var pathItems = PBXPath.Split(namespacePath); + foreach (var pathItem in pathItems) + { + folder = folder.OpenFolder(pathItem); + folder.providesNamespace = true; + } + return folder; + } + + public void Write() + { + Write(null); + } + + public void Write(List warnings) + { + m_Root.Write(warnings); + } + } + + internal abstract class AssetCatalogItem + { + public readonly string name; + public readonly string authorId; + public string path { get { return m_Path; } } + + protected Dictionary m_Properties = new Dictionary(); + + protected string m_Path; + + public AssetCatalogItem(string name, string authorId) + { + if (name != null && name.Contains("/")) + throw new Exception("Asset catalog item must not have slashes in name"); + this.name = name; + this.authorId = authorId; + } + + protected JsonElementDict WriteInfoToJson(JsonDocument doc) + { + var info = doc.root.CreateDict("info"); + info.SetInteger("version", 1); + info.SetString("author", authorId); + return info; + } + + public abstract void Write(List warnings); + } + + internal class AssetFolder : AssetCatalogItem + { + List m_Items = new List(); + bool m_ProvidesNamespace = false; + + public bool providesNamespace + { + get { return m_ProvidesNamespace; } + set { + if (m_Items.Count > 0 && value != m_ProvidesNamespace) + throw new Exception("Asset folder namespace providing status can't be "+ + "changed after items have been added"); + m_ProvidesNamespace = value; + } + } + + internal AssetFolder(string parentPath, string name, string authorId) : base(name, authorId) + { + if (name != null) + m_Path = Path.Combine(parentPath, name); + else + m_Path = parentPath; + } + + // Checks if a folder with given name exists and returns it if it does. + // Otherwise, creates a new folder. + public AssetFolder OpenFolder(string name) + { + var item = GetChild(name); + if (item != null) + { + if (item is AssetFolder) + return item as AssetFolder; + throw new Exception("The given path is already occupied with an asset"); + } + + var folder = new AssetFolder(m_Path, name, authorId); + m_Items.Add(folder); + return folder; + } + + T GetExistingItemWithType(string name) where T : class + { + var item = GetChild(name); + if (item != null) + { + if (item is T) + return item as T; + throw new Exception("The given path is already occupied with an asset"); + } + return null; + } + + // Checks if a dataset with given name exists and returns it if it does. + // Otherwise, creates a new data set. + public AssetDataSet OpenDataSet(string name) + { + var item = GetExistingItemWithType(name); + if (item != null) + return item; + + var dataset = new AssetDataSet(m_Path, name, authorId); + m_Items.Add(dataset); + return dataset; + } + + // Checks if an imageset with given name exists and returns it if it does. + // Otherwise, creates a new image set. + public AssetImageSet OpenImageSet(string name) + { + var item = GetExistingItemWithType(name); + if (item != null) + return item; + + var imageset = new AssetImageSet(m_Path, name, authorId); + m_Items.Add(imageset); + return imageset; + } + + // Checks if a image stack with given name exists and returns it if it does. + // Otherwise, creates a new image stack. + public AssetImageStack OpenImageStack(string name) + { + var item = GetExistingItemWithType(name); + if (item != null) + return item; + + var imageStack = new AssetImageStack(m_Path, name, authorId); + m_Items.Add(imageStack); + return imageStack; + } + + // Checks if a brand asset with given name exists and returns it if it does. + // Otherwise, creates a new brand asset. + public AssetBrandAssetGroup OpenBrandAssetGroup(string name) + { + var item = GetExistingItemWithType(name); + if (item != null) + return item; + + var brandAsset = new AssetBrandAssetGroup(m_Path, name, authorId); + m_Items.Add(brandAsset); + return brandAsset; + } + + // Returns the requested item or null if not found + public AssetCatalogItem GetChild(string name) + { + foreach (var item in m_Items) + { + if (item.name == name) + return item; + } + return null; + } + + void WriteJson() + { + if (!providesNamespace) + return; // json is optional when namespace is not provided + + var doc = new JsonDocument(); + + WriteInfoToJson(doc); + + var props = doc.root.CreateDict("properties"); + props.SetBoolean("provides-namespace", providesNamespace); + doc.WriteToFile(Path.Combine(m_Path, "Contents.json")); + } + + public override void Write(List warnings) + { + if (Directory.Exists(m_Path)) + Directory.Delete(m_Path, true); // ensure we start from clean state + Directory.CreateDirectory(m_Path); + WriteJson(); + + foreach (var item in m_Items) + item.Write(warnings); + } + } + + abstract class AssetCatalogItemWithVariants : AssetCatalogItem + { + protected List m_Variants = new List(); + protected List m_ODRTags = new List(); + + protected AssetCatalogItemWithVariants(string name, string authorId) : + base(name, authorId) + { + } + + protected class VariantData + { + public DeviceRequirement requirement; + public string path; + + public VariantData(DeviceRequirement requirement, string path) + { + this.requirement = requirement; + this.path = path; + } + } + + public bool HasVariant(DeviceRequirement requirement) + { + foreach (var item in m_Variants) + { + if (item.requirement.values == requirement.values) + return true; + } + return false; + } + + public void AddOnDemandResourceTag(string tag) + { + if (!m_ODRTags.Contains(tag)) + m_ODRTags.Add(tag); + } + + protected void AddVariant(VariantData newItem) + { + foreach (var item in m_Variants) + { + if (item.requirement.values == newItem.requirement.values) + throw new Exception("The given requirement has been already added"); + if (Path.GetFileName(item.path) == Path.GetFileName(path)) + throw new Exception("Two items within the same set must not have the same file name"); + } + if (Path.GetFileName(newItem.path) == "Contents.json") + throw new Exception("The file name must not be equal to Contents.json"); + m_Variants.Add(newItem); + } + + protected void WriteODRTagsToJson(JsonElementDict info) + { + if (m_ODRTags.Count > 0) + { + var tags = info.CreateArray("on-demand-resource-tags"); + foreach (var tag in m_ODRTags) + tags.AddString(tag); + } + } + + protected void WriteRequirementsToJson(JsonElementDict item, DeviceRequirement req) + { + foreach (var kv in req.values) + { + if (kv.Value != null && kv.Value != "") + item.SetString(kv.Key, kv.Value); + } + } + } + + internal class AssetDataSet : AssetCatalogItemWithVariants + { + class DataSetVariant : VariantData + { + public string id; + + public DataSetVariant(DeviceRequirement requirement, string path, string id) : base(requirement, path) + { + this.id = id; + } + } + + internal AssetDataSet(string parentPath, string name, string authorId) : base(name, authorId) + { + m_Path = Path.Combine(parentPath, name + ".dataset"); + } + + // an exception is thrown is two equivalent requirements are added. + // The same asset dataset must not have paths with equivalent filenames. + // The identifier allows to identify which data variant is actually loaded (use + // the typeIdentifer property of the NSDataAsset that was created from the data set) + public void AddVariant(DeviceRequirement requirement, string path, string typeIdentifier) + { + foreach (DataSetVariant item in m_Variants) + { + if (item.id != null && typeIdentifier != null && item.id == typeIdentifier) + throw new Exception("Two items within the same dataset must not have the same id"); + } + AddVariant(new DataSetVariant(requirement, path, typeIdentifier)); + } + + public override void Write(List warnings) + { + Directory.CreateDirectory(m_Path); + + var doc = new JsonDocument(); + + var info = WriteInfoToJson(doc); + WriteODRTagsToJson(info); + + var data = doc.root.CreateArray("data"); + + foreach (DataSetVariant item in m_Variants) + { + var filename = Path.GetFileName(item.path); + if (!File.Exists(item.path)) + { + if (warnings != null) + warnings.Add("File not found: " + item.path); + } + else + File.Copy(item.path, Path.Combine(m_Path, filename)); + + var docItem = data.AddDict(); + docItem.SetString("filename", filename); + WriteRequirementsToJson(docItem, item.requirement); + if (item.id != null) + docItem.SetString("universal-type-identifier", item.id); + } + doc.WriteToFile(Path.Combine(m_Path, "Contents.json")); + } + } + + internal class ImageAlignment + { + public int left = 0, right = 0, top = 0, bottom = 0; + } + + internal class ImageResizing + { + public enum SlicingType + { + Horizontal, + Vertical, + HorizontalAndVertical + } + + public enum ResizeMode + { + Stretch, + Tile + } + + public SlicingType type = SlicingType.HorizontalAndVertical; + public int left = 0; // only valid for horizontal slicing + public int right = 0; // only valid for horizontal slicing + public int top = 0; // only valid for vertical slicing + public int bottom = 0; // only valid for vertical slicing + public ResizeMode centerResizeMode = ResizeMode.Stretch; + public int centerWidth = 0; // only valid for vertical slicing + public int centerHeight = 0; // only valid for horizontal slicing + } + + // TODO: rendering intent property + internal class AssetImageSet : AssetCatalogItemWithVariants + { + internal AssetImageSet(string assetCatalogPath, string name, string authorId) : base(name, authorId) + { + m_Path = Path.Combine(assetCatalogPath, name + ".imageset"); + } + + class ImageSetVariant : VariantData + { + public ImageAlignment alignment = null; + public ImageResizing resizing = null; + + public ImageSetVariant(DeviceRequirement requirement, string path) : base(requirement, path) + { + } + } + + public void AddVariant(DeviceRequirement requirement, string path) + { + AddVariant(new ImageSetVariant(requirement, path)); + } + + public void AddVariant(DeviceRequirement requirement, string path, ImageAlignment alignment, ImageResizing resizing) + { + var imageset = new ImageSetVariant(requirement, path); + imageset.alignment = alignment; + imageset.resizing = resizing; + AddVariant(imageset); + } + + void WriteAlignmentToJson(JsonElementDict item, ImageAlignment alignment) + { + var docAlignment = item.CreateDict("alignment-insets"); + docAlignment.SetInteger("top", alignment.top); + docAlignment.SetInteger("bottom", alignment.bottom); + docAlignment.SetInteger("left", alignment.left); + docAlignment.SetInteger("right", alignment.right); + } + + static string GetSlicingMode(ImageResizing.SlicingType mode) + { + switch (mode) + { + case ImageResizing.SlicingType.Horizontal: return "3-part-horizontal"; + case ImageResizing.SlicingType.Vertical: return "3-part-vertical"; + case ImageResizing.SlicingType.HorizontalAndVertical: return "9-part"; + } + return ""; + } + + static string GetCenterResizeMode(ImageResizing.ResizeMode mode) + { + switch (mode) + { + case ImageResizing.ResizeMode.Stretch: return "stretch"; + case ImageResizing.ResizeMode.Tile: return "tile"; + } + return ""; + } + + void WriteResizingToJson(JsonElementDict item, ImageResizing resizing) + { + var docResizing = item.CreateDict("resizing"); + docResizing.SetString("mode", GetSlicingMode(resizing.type)); + + var docCenter = docResizing.CreateDict("center"); + docCenter.SetString("mode", GetCenterResizeMode(resizing.centerResizeMode)); + docCenter.SetInteger("width", resizing.centerWidth); + docCenter.SetInteger("height", resizing.centerHeight); + + var docInsets = docResizing.CreateDict("cap-insets"); + docInsets.SetInteger("top", resizing.top); + docInsets.SetInteger("bottom", resizing.bottom); + docInsets.SetInteger("left", resizing.left); + docInsets.SetInteger("right", resizing.right); + } + + public override void Write(List warnings) + { + Directory.CreateDirectory(m_Path); + var doc = new JsonDocument(); + var info = WriteInfoToJson(doc); + WriteODRTagsToJson(info); + + var images = doc.root.CreateArray("images"); + + foreach (ImageSetVariant item in m_Variants) + { + var filename = Path.GetFileName(item.path); + if (!File.Exists(item.path)) + { + if (warnings != null) + warnings.Add("File not found: " + item.path); + } + else + File.Copy(item.path, Path.Combine(m_Path, filename)); + + var docItem = images.AddDict(); + docItem.SetString("filename", filename); + WriteRequirementsToJson(docItem, item.requirement); + if (item.alignment != null) + WriteAlignmentToJson(docItem, item.alignment); + if (item.resizing != null) + WriteResizingToJson(docItem, item.resizing); + } + doc.WriteToFile(Path.Combine(m_Path, "Contents.json")); + } + } + + /* A stack layer may either contain an image set or reference another imageset + */ + class AssetImageStackLayer : AssetCatalogItem + { + internal AssetImageStackLayer(string assetCatalogPath, string name, string authorId) : base(name, authorId) + { + m_Path = Path.Combine(assetCatalogPath, name + ".imagestacklayer"); + m_Imageset = new AssetImageSet(m_Path, "Content", authorId); + } + + AssetImageSet m_Imageset = null; + string m_ReferencedName = null; + + public void SetReference(string name) + { + m_Imageset = null; + m_ReferencedName = name; + } + + public string ReferencedName() + { + return m_ReferencedName; + } + + public AssetImageSet GetImageSet() + { + return m_Imageset; + } + + public override void Write(List warnings) + { + Directory.CreateDirectory(m_Path); + var doc = new JsonDocument(); + WriteInfoToJson(doc); + + if (m_ReferencedName != null) + { + var props = doc.root.CreateDict("properties"); + var reference = props.CreateDict("content-reference"); + reference.SetString("type", "image-set"); + reference.SetString("name", m_ReferencedName); + reference.SetString("matching-style", "fully-qualified-name"); + } + if (m_Imageset != null) + m_Imageset.Write(warnings); + + doc.WriteToFile(Path.Combine(m_Path, "Contents.json")); + } + } + + class AssetImageStack : AssetCatalogItem + { + List m_Layers = new List(); + + internal AssetImageStack(string assetCatalogPath, string name, string authorId) : base(name, authorId) + { + m_Path = Path.Combine(assetCatalogPath, name + ".imagestack"); + } + + public AssetImageStackLayer AddLayer(string name) + { + foreach (var layer in m_Layers) + { + if (layer.name == name) + throw new Exception("A layer with given name already exists"); + } + var newLayer = new AssetImageStackLayer(m_Path, name, authorId); + m_Layers.Add(newLayer); + return newLayer; + } + + public override void Write(List warnings) + { + Directory.CreateDirectory(m_Path); + var doc = new JsonDocument(); + WriteInfoToJson(doc); + + var docLayers = doc.root.CreateArray("layers"); + foreach (var layer in m_Layers) + { + layer.Write(warnings); + + var docLayer = docLayers.AddDict(); + docLayer.SetString("filename", Path.GetFileName(layer.path)); + } + doc.WriteToFile(Path.Combine(m_Path, "Contents.json")); + } + } + + class AssetBrandAssetGroup : AssetCatalogItem + { + class AssetBrandAssetItem + { + internal string idiom = null; + internal string role = null; + internal int width, height; + internal AssetCatalogItem item = null; + + } + + List m_Items = new List(); + + internal AssetBrandAssetGroup(string assetCatalogPath, string name, string authorId) : base(name, authorId) + { + m_Path = Path.Combine(assetCatalogPath, name + ".brandassets"); + } + + void AddItem(AssetCatalogItem item, string idiom, string role, int width, int height) + { + foreach (var it in m_Items) + { + if (it.item.name == item.name) + throw new Exception("An item with given name already exists"); + } + var newItem = new AssetBrandAssetItem(); + newItem.item = item; + newItem.idiom = idiom; + newItem.role = role; + newItem.width = width; + newItem.height = height; + m_Items.Add(newItem); + } + + public AssetImageSet OpenImageSet(string name, string idiom, string role, int width, int height) + { + var newItem = new AssetImageSet(m_Path, name, authorId); + AddItem(newItem, idiom, role, width, height); + return newItem; + } + + public AssetImageStack OpenImageStack(string name, string idiom, string role, int width, int height) + { + var newItem = new AssetImageStack(m_Path, name, authorId); + AddItem(newItem, idiom, role, width, height); + return newItem; + } + + public override void Write(List warnings) + { + Directory.CreateDirectory(m_Path); + var doc = new JsonDocument(); + WriteInfoToJson(doc); + + var docAssets = doc.root.CreateArray("assets"); + foreach (var item in m_Items) + { + var docAsset = docAssets.AddDict(); + docAsset.SetString("size", String.Format("{0}x{1}", item.width, item.height)); + docAsset.SetString("idiom", item.idiom); + docAsset.SetString("role", item.role); + docAsset.SetString("filename", Path.GetFileName(item.item.path)); + + item.item.Write(warnings); + } + doc.WriteToFile(Path.Combine(m_Path, "Contents.json")); + } + } + +} // namespace UnityEditor.iOS.Xcode diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/AssetCatalog.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/AssetCatalog.cs.meta new file mode 100644 index 0000000..31465df --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/AssetCatalog.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 46dd33873affe4e5caf86cb784bd831a +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/JsonParser.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/JsonParser.cs new file mode 100644 index 0000000..b54ba98 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/JsonParser.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Linq; + +namespace ChillyRoom.UnityEditor.iOS.Xcode +{ + internal class JsonElement + { + protected JsonElement() {} + + // convenience methods + public string AsString() { return ((JsonElementString)this).value; } + public int AsInteger() { return ((JsonElementInteger)this).value; } + public bool AsBoolean() { return ((JsonElementBoolean)this).value; } + public JsonElementArray AsArray() { return (JsonElementArray)this; } + public JsonElementDict AsDict() { return (JsonElementDict)this; } + + public JsonElement this[string key] + { + get { return AsDict()[key]; } + set { AsDict()[key] = value; } + } + } + + internal class JsonElementString : JsonElement + { + public JsonElementString(string v) { value = v; } + + public string value; + } + + internal class JsonElementInteger : JsonElement + { + public JsonElementInteger(int v) { value = v; } + + public int value; + } + + internal class JsonElementBoolean : JsonElement + { + public JsonElementBoolean(bool v) { value = v; } + + public bool value; + } + + internal class JsonElementDict : JsonElement + { + public JsonElementDict() : base() {} + + private SortedDictionary m_PrivateValue = new SortedDictionary(); + public IDictionary values { get { return m_PrivateValue; }} + + new public JsonElement this[string key] + { + get { + if (values.ContainsKey(key)) + return values[key]; + return null; + } + set { this.values[key] = value; } + } + + public bool Contains(string key) + { + return values.ContainsKey(key); + } + + public void Remove(string key) + { + values.Remove(key); + } + + // convenience methods + public void SetInteger(string key, int val) + { + values[key] = new JsonElementInteger(val); + } + + public void SetString(string key, string val) + { + values[key] = new JsonElementString(val); + } + + public void SetBoolean(string key, bool val) + { + values[key] = new JsonElementBoolean(val); + } + + public JsonElementArray CreateArray(string key) + { + var v = new JsonElementArray(); + values[key] = v; + return v; + } + + public JsonElementDict CreateDict(string key) + { + var v = new JsonElementDict(); + values[key] = v; + return v; + } + } + + internal class JsonElementArray : JsonElement + { + public JsonElementArray() : base() {} + public List values = new List(); + + // convenience methods + public void AddString(string val) + { + values.Add(new JsonElementString(val)); + } + + public void AddInteger(int val) + { + values.Add(new JsonElementInteger(val)); + } + + public void AddBoolean(bool val) + { + values.Add(new JsonElementBoolean(val)); + } + + public JsonElementArray AddArray() + { + var v = new JsonElementArray(); + values.Add(v); + return v; + } + + public JsonElementDict AddDict() + { + var v = new JsonElementDict(); + values.Add(v); + return v; + } + } + + internal class JsonDocument + { + public JsonElementDict root; + public string indentString = " "; + + public JsonDocument() + { + root = new JsonElementDict(); + } + + void AppendIndent(StringBuilder sb, int indent) + { + for (int i = 0; i < indent; ++i) + sb.Append(indentString); + } + + void WriteString(StringBuilder sb, string str) + { + // TODO: escape + sb.Append('"'); + sb.Append(str); + sb.Append('"'); + } + + void WriteBoolean(StringBuilder sb, bool value) + { + sb.Append(value ? "true" : "false"); + } + + void WriteInteger(StringBuilder sb, int value) + { + sb.Append(value.ToString()); + } + + void WriteDictKeyValue(StringBuilder sb, string key, JsonElement value, int indent) + { + sb.Append("\n"); + AppendIndent(sb, indent); + WriteString(sb, key); + sb.Append(" : "); + if (value is JsonElementString) + WriteString(sb, value.AsString()); + else if (value is JsonElementInteger) + WriteInteger(sb, value.AsInteger()); + else if (value is JsonElementBoolean) + WriteBoolean(sb, value.AsBoolean()); + else if (value is JsonElementDict) + WriteDict(sb, value.AsDict(), indent); + else if (value is JsonElementArray) + WriteArray(sb, value.AsArray(), indent); + } + + void WriteDict(StringBuilder sb, JsonElementDict el, int indent) + { + sb.Append("{"); + bool hasElement = false; + foreach (var key in el.values.Keys) + { + if (hasElement) + sb.Append(","); // trailing commas not supported + WriteDictKeyValue(sb, key, el[key], indent+1); + hasElement = true; + } + sb.Append("\n"); + AppendIndent(sb, indent); + sb.Append("}"); + } + + void WriteArray(StringBuilder sb, JsonElementArray el, int indent) + { + sb.Append("["); + bool hasElement = false; + foreach (var value in el.values) + { + if (hasElement) + sb.Append(","); // trailing commas not supported + sb.Append("\n"); + AppendIndent(sb, indent+1); + + if (value is JsonElementString) + WriteString(sb, value.AsString()); + else if (value is JsonElementInteger) + WriteInteger(sb, value.AsInteger()); + else if (value is JsonElementBoolean) + WriteBoolean(sb, value.AsBoolean()); + else if (value is JsonElementDict) + WriteDict(sb, value.AsDict(), indent+1); + else if (value is JsonElementArray) + WriteArray(sb, value.AsArray(), indent+1); + hasElement = true; + } + sb.Append("\n"); + AppendIndent(sb, indent); + sb.Append("]"); + } + + public void WriteToFile(string path) + { + File.WriteAllText(path, WriteToString()); + } + + public void WriteToStream(TextWriter tw) + { + tw.Write(WriteToString()); + } + + public string WriteToString() + { + var sb = new StringBuilder(); + WriteDict(sb, root, 0); + return sb.ToString(); + } + } + + +} // namespace UnityEditor.iOS.Xcode \ No newline at end of file diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/JsonParser.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/JsonParser.cs.meta new file mode 100644 index 0000000..d325bf8 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/JsonParser.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 052ac8a8c9ae3466f94f7b3b6c5b61af +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX.meta new file mode 100644 index 0000000..cd39a5d --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 7625017be5ff149a6b0f20ee645ce11e +folderAsset: yes +timeCreated: 1496741689 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Elements.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Elements.cs new file mode 100644 index 0000000..5be13a9 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Elements.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Collections; +using System; + + +namespace ChillyRoom.UnityEditor.iOS.Xcode.PBX +{ + + class PBXElement + { + protected PBXElement() {} + + // convenience methods + public string AsString() { return ((PBXElementString)this).value; } + public PBXElementArray AsArray() { return (PBXElementArray)this; } + public PBXElementDict AsDict() { return (PBXElementDict)this; } + + public PBXElement this[string key] + { + get { return AsDict()[key]; } + set { AsDict()[key] = value; } + } + } + + class PBXElementString : PBXElement + { + public PBXElementString(string v) { value = v; } + + public string value; + } + + class PBXElementDict : PBXElement + { + public PBXElementDict() : base() {} + + private Dictionary m_PrivateValue = new Dictionary(); + public IDictionary values { get { return m_PrivateValue; }} + + new public PBXElement this[string key] + { + get { + if (values.ContainsKey(key)) + return values[key]; + return null; + } + set { this.values[key] = value; } + } + + public bool Contains(string key) + { + return values.ContainsKey(key); + } + + public void Remove(string key) + { + values.Remove(key); + } + + public void SetString(string key, string val) + { + values[key] = new PBXElementString(val); + } + + public PBXElementArray CreateArray(string key) + { + var v = new PBXElementArray(); + values[key] = v; + return v; + } + + public PBXElementDict CreateDict(string key) + { + var v = new PBXElementDict(); + values[key] = v; + return v; + } + } + + class PBXElementArray : PBXElement + { + public PBXElementArray() : base() {} + public List values = new List(); + + // convenience methods + public void AddString(string val) + { + values.Add(new PBXElementString(val)); + } + + public PBXElementArray AddArray() + { + var v = new PBXElementArray(); + values.Add(v); + return v; + } + + public PBXElementDict AddDict() + { + var v = new PBXElementDict(); + values.Add(v); + return v; + } + } + +} // namespace UnityEditor.iOS.Xcode + diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Elements.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Elements.cs.meta new file mode 100644 index 0000000..b757dea --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Elements.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 83cffec4be0e045258109e9714cdaa5f +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Lexer.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Lexer.cs new file mode 100644 index 0000000..c7c320b --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Lexer.cs @@ -0,0 +1,243 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.IO; +using System.Linq; +using System; + + +namespace ChillyRoom.UnityEditor.iOS.Xcode.PBX +{ + enum TokenType + { + EOF, + Invalid, + String, + QuotedString, + Comment, + + Semicolon, // ; + Comma, // , + Eq, // = + LParen, // ( + RParen, // ) + LBrace, // { + RBrace, // } + } + + class Token + { + public TokenType type; + + // the line of the input stream the token starts in (0-based) + public int line; + + // start and past-the-end positions of the token in the input stream + public int begin, end; + } + + class TokenList : List + { + } + + class Lexer + { + string text; + int pos; + int length; + int line; + + public static TokenList Tokenize(string text) + { + var lexer = new Lexer(); + lexer.SetText(text); + return lexer.ScanAll(); + } + + public void SetText(string text) + { + this.text = text + " "; // to prevent out-of-bounds access during look ahead + pos = 0; + length = text.Length; + line = 0; + } + + public TokenList ScanAll() + { + var tokens = new TokenList(); + + while (true) + { + var tok = new Token(); + ScanOne(tok); + tokens.Add(tok); + if (tok.type == TokenType.EOF) + break; + } + return tokens; + } + + void UpdateNewlineStats(char ch) + { + if (ch == '\n') + line++; + } + + // tokens list is modified in the case when we add BrokenLine token and need to remove already + // added tokens for the current line + void ScanOne(Token tok) + { + while (true) + { + while (pos < length && Char.IsWhiteSpace(text[pos])) + { + UpdateNewlineStats(text[pos]); + pos++; + } + + if (pos >= length) + { + tok.type = TokenType.EOF; + break; + } + + char ch = text[pos]; + char ch2 = text[pos+1]; + + if (ch == '\"') + ScanQuotedString(tok); + else if (ch == '/' && ch2 == '*') + ScanMultilineComment(tok); + else if (ch == '/' && ch2 == '/') + ScanComment(tok); + else if (IsOperator(ch)) + ScanOperator(tok); + else + ScanString(tok); // be more robust and accept whatever is left + return; + } + } + + void ScanString(Token tok) + { + tok.type = TokenType.String; + tok.begin = pos; + while (pos < length) + { + char ch = text[pos]; + char ch2 = text[pos+1]; + + if (Char.IsWhiteSpace(ch)) + break; + else if (ch == '\"') + break; + else if (ch == '/' && ch2 == '*') + break; + else if (ch == '/' && ch2 == '/') + break; + else if (IsOperator(ch)) + break; + pos++; + } + tok.end = pos; + tok.line = line; + } + + void ScanQuotedString(Token tok) + { + tok.type = TokenType.QuotedString; + tok.begin = pos; + pos++; + + while (pos < length) + { + // ignore escaped quotes + if (text[pos] == '\\' && text[pos+1] == '\"') + { + pos += 2; + continue; + } + + // note that we close unclosed quotes + if (text[pos] == '\"') + break; + + UpdateNewlineStats(text[pos]); + pos++; + } + pos++; + tok.end = pos; + tok.line = line; + } + + void ScanMultilineComment(Token tok) + { + tok.type = TokenType.Comment; + tok.begin = pos; + pos += 2; + + while (pos < length) + { + if (text[pos] == '*' && text[pos+1] == '/') + break; + + // we support multiline comments + UpdateNewlineStats(text[pos]); + pos++; + } + pos += 2; + tok.end = pos; + tok.line = line; + } + + void ScanComment(Token tok) + { + tok.type = TokenType.Comment; + tok.begin = pos; + pos += 2; + + while (pos < length) + { + if (text[pos] == '\n') + break; + pos++; + } + UpdateNewlineStats(text[pos]); + pos++; + tok.end = pos; + tok.line = line; + } + + bool IsOperator(char ch) + { + if (ch == ';' || ch == ',' || ch == '=' || ch == '(' || ch == ')' || ch == '{' || ch == '}') + return true; + return false; + } + + void ScanOperator(Token tok) + { + switch (text[pos]) + { + case ';': ScanOperatorSpecific(tok, TokenType.Semicolon); return; + case ',': ScanOperatorSpecific(tok, TokenType.Comma); return; + case '=': ScanOperatorSpecific(tok, TokenType.Eq); return; + case '(': ScanOperatorSpecific(tok, TokenType.LParen); return; + case ')': ScanOperatorSpecific(tok, TokenType.RParen); return; + case '{': ScanOperatorSpecific(tok, TokenType.LBrace); return; + case '}': ScanOperatorSpecific(tok, TokenType.RBrace); return; + default: return; + } + } + + void ScanOperatorSpecific(Token tok, TokenType type) + { + tok.type = type; + tok.begin = pos; + pos++; + tok.end = pos; + tok.line = line; + } + } + + +} // namespace UnityEditor.iOS.Xcode \ No newline at end of file diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Lexer.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Lexer.cs.meta new file mode 100644 index 0000000..9a985d8 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Lexer.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9766f6c4eb5ef42f99c0d5f86e34ba84 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Objects.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Objects.cs new file mode 100644 index 0000000..3b790f7 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Objects.cs @@ -0,0 +1,1007 @@ +using System.Collections.Generic; +using System.Collections; +using System.Text.RegularExpressions; +using System.IO; +using System.Linq; +using System; + + +namespace ChillyRoom.UnityEditor.iOS.Xcode.PBX +{ + internal class PBXObjectData + { + public string guid; + protected PBXElementDict m_Properties = new PBXElementDict(); + + internal void SetPropertiesWhenSerializing(PBXElementDict props) + { + m_Properties = props; + } + + internal PBXElementDict GetPropertiesWhenSerializing() + { + return m_Properties; + } + + /* Returns the internal properties dictionary which the user may manipulate directly. + If any of the properties are modified, UpdateVars() must be called before any other + operation affecting the given Xcode document is executed. + */ + internal PBXElementDict GetPropertiesRaw() + { + UpdateProps(); + return m_Properties; + } + + // returns null if it does not exist + protected string GetPropertyString(string name) + { + var prop = m_Properties[name]; + if (prop == null) + return null; + + return prop.AsString(); + } + + protected void SetPropertyString(string name, string value) + { + if (value == null) + m_Properties.Remove(name); + else + m_Properties.SetString(name, value); + } + + protected List GetPropertyList(string name) + { + var prop = m_Properties[name]; + if (prop == null) + return null; + + var list = new List(); + foreach (var el in prop.AsArray().values) + list.Add(el.AsString()); + return list; + } + + protected void SetPropertyList(string name, List value) + { + if (value == null) + m_Properties.Remove(name); + else + { + var array = m_Properties.CreateArray(name); + foreach (string val in value) + array.AddString(val); + } + } + + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(); + internal virtual PropertyCommentChecker checker { get { return checkerData; } } + internal virtual bool shouldCompact { get { return false; } } + + public virtual void UpdateProps() {} // Updates the props from cached variables + public virtual void UpdateVars() {} // Updates the cached variables from underlying props + } + + internal class PBXBuildFileData : PBXObjectData + { + public string fileRef; + public string compileFlags; + public bool weak; + public List assetTags; + + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "fileRef/*" + }); + internal override PropertyCommentChecker checker { get { return checkerData; } } + internal override bool shouldCompact { get { return true; } } + + public static PBXBuildFileData CreateFromFile(string fileRefGUID, bool weak, + string compileFlags) + { + PBXBuildFileData buildFile = new PBXBuildFileData(); + buildFile.guid = PBXGUID.Generate(); + buildFile.SetPropertyString("isa", "PBXBuildFile"); + buildFile.fileRef = fileRefGUID; + buildFile.compileFlags = compileFlags; + buildFile.weak = weak; + buildFile.assetTags = new List(); + return buildFile; + } + + public override void UpdateProps() + { + SetPropertyString("fileRef", fileRef); + + PBXElementDict settings = null; + if (m_Properties.Contains("settings")) + settings = m_Properties["settings"].AsDict(); + + if (compileFlags != null && compileFlags != "") + { + if (settings == null) + settings = m_Properties.CreateDict("settings"); + settings.SetString("COMPILER_FLAGS", compileFlags); + } + else + { + if (settings != null) + settings.Remove("COMPILER_FLAGS"); + } + + if (weak) + { + if (settings == null) + settings = m_Properties.CreateDict("settings"); + PBXElementArray attrs = null; + if (settings.Contains("ATTRIBUTES")) + attrs = settings["ATTRIBUTES"].AsArray(); + else + attrs = settings.CreateArray("ATTRIBUTES"); + + bool exists = false; + foreach (var value in attrs.values) + { + if (value is PBXElementString && value.AsString() == "Weak") + exists = true; + } + if (!exists) + attrs.AddString("Weak"); + } + else + { + if (settings != null && settings.Contains("ATTRIBUTES")) + { + var attrs = settings["ATTRIBUTES"].AsArray(); + attrs.values.RemoveAll(el => (el is PBXElementString && el.AsString() == "Weak")); + if (attrs.values.Count == 0) + settings.Remove("ATTRIBUTES"); + } + } + + if (assetTags.Count > 0) + { + if (settings == null) + settings = m_Properties.CreateDict("settings"); + var tagsArray = settings.CreateArray("ASSET_TAGS"); + foreach (string tag in assetTags) + tagsArray.AddString(tag); + } + else + { + if (settings != null) + settings.Remove("ASSET_TAGS"); + } + + if (settings != null && settings.values.Count == 0) + m_Properties.Remove("settings"); + } + + public override void UpdateVars() + { + fileRef = GetPropertyString("fileRef"); + compileFlags = null; + weak = false; + assetTags = new List(); + if (m_Properties.Contains("settings")) + { + var dict = m_Properties["settings"].AsDict(); + if (dict.Contains("COMPILER_FLAGS")) + compileFlags = dict["COMPILER_FLAGS"].AsString(); + + if (dict.Contains("ATTRIBUTES")) + { + var attrs = dict["ATTRIBUTES"].AsArray(); + foreach (var value in attrs.values) + { + if (value is PBXElementString && value.AsString() == "Weak") + weak = true; + } + } + if (dict.Contains("ASSET_TAGS")) + { + var tags = dict["ASSET_TAGS"].AsArray(); + foreach (var value in tags.values) + assetTags.Add(value.AsString()); + } + } + } + } + + internal class PBXFileReferenceData : PBXObjectData + { + string m_Path = null; + string m_ExplicitFileType = null; + string m_LastKnownFileType = null; + + public string path + { + get { return m_Path; } + set { m_ExplicitFileType = null; m_LastKnownFileType = null; m_Path = value; } + } + + public string name; + public PBXSourceTree tree; + public bool isFolderReference + { + get { return m_LastKnownFileType != null && m_LastKnownFileType == "folder"; } + } + + internal override bool shouldCompact { get { return true; } } + + public static PBXFileReferenceData CreateFromFile(string path, string projectFileName, + PBXSourceTree tree) + { + string guid = PBXGUID.Generate(); + + PBXFileReferenceData fileRef = new PBXFileReferenceData(); + fileRef.SetPropertyString("isa", "PBXFileReference"); + fileRef.guid = guid; + fileRef.path = path; + fileRef.name = projectFileName; + fileRef.tree = tree; + return fileRef; + } + + public static PBXFileReferenceData CreateFromFolderReference(string path, string projectFileName, + PBXSourceTree tree) + { + var fileRef = CreateFromFile(path, projectFileName, tree); + fileRef.m_LastKnownFileType = "folder"; + return fileRef; + } + + public override void UpdateProps() + { + string ext = null; + if (m_ExplicitFileType != null) + SetPropertyString("explicitFileType", m_ExplicitFileType); + else if (m_LastKnownFileType != null) + SetPropertyString("lastKnownFileType", m_LastKnownFileType); + else + { + if (name != null) + ext = Path.GetExtension(name); + else if (m_Path != null) + ext = Path.GetExtension(m_Path); + if (ext != null) + { + if (FileTypeUtils.IsFileTypeExplicit(ext)) + SetPropertyString("explicitFileType", FileTypeUtils.GetTypeName(ext)); + else + SetPropertyString("lastKnownFileType", FileTypeUtils.GetTypeName(ext)); + } + } + if (m_Path == name) + SetPropertyString("name", null); + else + SetPropertyString("name", name); + if (m_Path == null) + SetPropertyString("path", ""); + else + SetPropertyString("path", m_Path); + SetPropertyString("sourceTree", FileTypeUtils.SourceTreeDesc(tree)); + } + + public override void UpdateVars() + { + name = GetPropertyString("name"); + m_Path = GetPropertyString("path"); + if (name == null) + name = m_Path; + if (m_Path == null) + m_Path = ""; + tree = FileTypeUtils.ParseSourceTree(GetPropertyString("sourceTree")); + m_ExplicitFileType = GetPropertyString("explicitFileType"); + m_LastKnownFileType = GetPropertyString("lastKnownFileType"); + } + } + + class GUIDList : IEnumerable + { + private List m_List = new List(); + + public GUIDList() {} + public GUIDList(List data) + { + m_List = data; + } + + public static implicit operator List(GUIDList list) { return list.m_List; } + public static implicit operator GUIDList(List data) { return new GUIDList(data); } + + public void AddGUID(string guid) { m_List.Add(guid); } + public void RemoveGUID(string guid) { m_List.RemoveAll(x => x == guid); } + public bool Contains(string guid) { return m_List.Contains(guid); } + public int Count { get { return m_List.Count; } } + public void Clear() { m_List.Clear(); } + IEnumerator IEnumerable.GetEnumerator() { return m_List.GetEnumerator(); } + IEnumerator IEnumerable.GetEnumerator() { return m_List.GetEnumerator(); } + } + + internal class XCConfigurationListData : PBXObjectData + { + public GUIDList buildConfigs; + + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "buildConfigurations/*" + }); + internal override PropertyCommentChecker checker { get { return checkerData; } } + + public static XCConfigurationListData Create() + { + var res = new XCConfigurationListData(); + res.guid = PBXGUID.Generate(); + + res.SetPropertyString("isa", "XCConfigurationList"); + res.buildConfigs = new GUIDList(); + res.SetPropertyString("defaultConfigurationIsVisible", "0"); + + return res; + } + + public override void UpdateProps() + { + SetPropertyList("buildConfigurations", buildConfigs); + } + public override void UpdateVars() + { + buildConfigs = GetPropertyList("buildConfigurations"); + } + } + + internal class PBXGroupData : PBXObjectData + { + public GUIDList children; + public PBXSourceTree tree; + public string name, path; + + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "children/*" + }); + internal override PropertyCommentChecker checker { get { return checkerData; } } + + // name must not contain '/' + public static PBXGroupData Create(string name, string path, PBXSourceTree tree) + { + if (name.Contains("/")) + throw new Exception("Group name must not contain '/'"); + + PBXGroupData gr = new PBXGroupData(); + gr.guid = PBXGUID.Generate(); + gr.SetPropertyString("isa", "PBXGroup"); + gr.name = name; + gr.path = path; + gr.tree = PBXSourceTree.Group; + gr.children = new GUIDList(); + + return gr; + } + + public static PBXGroupData CreateRelative(string name) + { + return Create(name, name, PBXSourceTree.Group); + } + + public override void UpdateProps() + { + // The name property is set only if it is different from the path property + SetPropertyList("children", children); + if (name == path) + SetPropertyString("name", null); + else + SetPropertyString("name", name); + if (path == "") + SetPropertyString("path", null); + else + SetPropertyString("path", path); + SetPropertyString("sourceTree", FileTypeUtils.SourceTreeDesc(tree)); + } + public override void UpdateVars() + { + children = GetPropertyList("children"); + path = GetPropertyString("path"); + name = GetPropertyString("name"); + if (name == null) + name = path; + if (path == null) + path = ""; + tree = FileTypeUtils.ParseSourceTree(GetPropertyString("sourceTree")); + } + } + + internal class PBXVariantGroupData : PBXGroupData + { + public static PBXVariantGroupData Create (string name, PBXSourceTree tree) + { + if (name.Contains("/")) + throw new Exception ("Group name must not contain '/'"); + + PBXVariantGroupData gr = new PBXVariantGroupData (); + gr.guid = PBXGUID.Generate(); + gr.SetPropertyString("isa", "PBXVariantGroup"); + gr.name = name; + gr.tree = PBXSourceTree.Group; + gr.children = new GUIDList (); + return gr; + } + } + + internal class PBXNativeTargetData : PBXObjectData + { + public GUIDList phases; + + public string buildConfigList; // guid + public string name; + public GUIDList dependencies; + public string productReference; // guid + + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "buildPhases/*", + "buildRules/*", + "dependencies/*", + "productReference/*", + "buildConfigurationList/*" + }); + + internal override PropertyCommentChecker checker { get { return checkerData; } } + + public static PBXNativeTargetData Create(string name, string productRef, + string productType, string buildConfigList) + { + var res = new PBXNativeTargetData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXNativeTarget"); + res.buildConfigList = buildConfigList; + res.phases = new GUIDList(); + res.SetPropertyList("buildRules", new List()); + res.dependencies = new GUIDList(); + res.name = name; + res.productReference = productRef; + res.SetPropertyString("productName", name); + res.SetPropertyString("productReference", productRef); + res.SetPropertyString("productType", productType); + return res; + } + + public override void UpdateProps() + { + SetPropertyString("buildConfigurationList", buildConfigList); + SetPropertyString("name", name); + SetPropertyString("productReference", productReference); + SetPropertyList("buildPhases", phases); + SetPropertyList("dependencies", dependencies); + } + public override void UpdateVars() + { + buildConfigList = GetPropertyString("buildConfigurationList"); + name = GetPropertyString("name"); + productReference = GetPropertyString("productReference"); + phases = GetPropertyList("buildPhases"); + dependencies = GetPropertyList("dependencies"); + } + } + + + internal class FileGUIDListBase : PBXObjectData + { + public GUIDList files; + + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "files/*", + }); + + internal override PropertyCommentChecker checker { get { return checkerData; } } + + public override void UpdateProps() + { + SetPropertyList("files", files); + } + public override void UpdateVars() + { + files = GetPropertyList("files"); + } + } + + internal class PBXSourcesBuildPhaseData : FileGUIDListBase + { + public static PBXSourcesBuildPhaseData Create() + { + var res = new PBXSourcesBuildPhaseData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXSourcesBuildPhase"); + res.SetPropertyString("buildActionMask", "2147483647"); + res.files = new List(); + res.SetPropertyString("runOnlyForDeploymentPostprocessing", "0"); + return res; + } + } + + internal class PBXFrameworksBuildPhaseData : FileGUIDListBase + { + public static PBXFrameworksBuildPhaseData Create() + { + var res = new PBXFrameworksBuildPhaseData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXFrameworksBuildPhase"); + res.SetPropertyString("buildActionMask", "2147483647"); + res.files = new List(); + res.SetPropertyString("runOnlyForDeploymentPostprocessing", "0"); + return res; + } + } + + internal class PBXResourcesBuildPhaseData : FileGUIDListBase + { + public static PBXResourcesBuildPhaseData Create() + { + var res = new PBXResourcesBuildPhaseData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXResourcesBuildPhase"); + res.SetPropertyString("buildActionMask", "2147483647"); + res.files = new List(); + res.SetPropertyString("runOnlyForDeploymentPostprocessing", "0"); + return res; + } + } + + internal class PBXCopyFilesBuildPhaseData : FileGUIDListBase + { + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "files/*", + }); + + internal override PropertyCommentChecker checker { get { return checkerData; } } + + public string name; + + // name may be null + public static PBXCopyFilesBuildPhaseData Create(string name, string dstPath, string subfolderSpec) + { + var res = new PBXCopyFilesBuildPhaseData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXCopyFilesBuildPhase"); + res.SetPropertyString("buildActionMask", "2147483647"); + res.SetPropertyString("dstPath", dstPath); + res.SetPropertyString("dstSubfolderSpec", subfolderSpec); + res.files = new List(); + res.SetPropertyString("runOnlyForDeploymentPostprocessing", "0"); + res.name = name; + return res; + } + + public override void UpdateProps() + { + SetPropertyList("files", files); + SetPropertyString("name", name); + } + public override void UpdateVars() + { + files = GetPropertyList("files"); + name = GetPropertyString("name"); + } + } + + internal class PBXShellScriptBuildPhaseData : FileGUIDListBase + { + public string name; + public string shellPath; + public string shellScript; + + public static PBXShellScriptBuildPhaseData Create(string name, string shellPath, string shellScript) + { + var res = new PBXShellScriptBuildPhaseData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXShellScriptBuildPhase"); + res.SetPropertyString("buildActionMask", "2147483647"); + res.files = new List(); + res.SetPropertyString("runOnlyForDeploymentPostprocessing", "0"); + res.name = name; + res.shellPath = shellPath; + res.shellScript = shellScript; + return res; + } + + public override void UpdateProps() + { + base.UpdateProps(); + SetPropertyString("name", name); + SetPropertyString("shellPath", shellPath); + SetPropertyString("shellScript", shellScript); + } + public override void UpdateVars() + { + base.UpdateVars(); + name = GetPropertyString("name"); + shellPath = GetPropertyString("shellPath"); + shellScript = GetPropertyString("shellScript"); + } + } + + internal class BuildConfigEntryData + { + public string name; + public List val = new List(); + + public static string ExtractValue(string src) + { + return PBXStream.UnquoteString(src.Trim().TrimEnd(',')); + } + + public void AddValue(string value) + { + if (!val.Contains(value)) + val.Add(value); + } + + public void RemoveValue(string value) + { + val.RemoveAll(v => v == value); + } + + public void RemoveValueList(IEnumerable values) + { + List valueList = new List(values); + if (valueList.Count == 0) + return; + for (int i = 0; i < val.Count - valueList.Count; i++) + { + bool match = true; + for (int j = 0; j < valueList.Count; j++) + { + if (val[i + j] != valueList[j]) + { + match = false; + break; + } + } + if (match) + { + val.RemoveRange(i, valueList.Count); + return; + } + } + } + + public static BuildConfigEntryData FromNameValue(string name, string value) + { + BuildConfigEntryData ret = new BuildConfigEntryData(); + ret.name = name; + ret.AddValue(value); + return ret; + } + } + + internal class XCBuildConfigurationData : PBXObjectData + { + protected SortedDictionary entries = new SortedDictionary(); + public string name { get { return GetPropertyString("name"); } } + public string baseConfigurationReference; // may be null + + // Note that QuoteStringIfNeeded does its own escaping. Double-escaping with quotes is + // required to please Xcode that does not handle paths with spaces if they are not + // enclosed in quotes. + static string EscapeWithQuotesIfNeeded(string name, string value) + { + if (name != "LIBRARY_SEARCH_PATHS" && name != "FRAMEWORK_SEARCH_PATHS") + return value; + if (!value.Contains(" ")) + return value; + if (value.First() == '\"' && value.Last() == '\"') + return value; + return "\"" + value + "\""; + } + + public void SetProperty(string name, string value) + { + entries[name] = BuildConfigEntryData.FromNameValue(name, EscapeWithQuotesIfNeeded(name, value)); + } + + public void AddProperty(string name, string value) + { + if (entries.ContainsKey(name)) + entries[name].AddValue(EscapeWithQuotesIfNeeded(name, value)); + else + SetProperty(name, value); + } + + public void RemoveProperty(string name) + { + if (entries.ContainsKey(name)) + entries.Remove(name); + } + + public void RemovePropertyValue(string name, string value) + { + if (entries.ContainsKey(name)) + entries[name].RemoveValue(EscapeWithQuotesIfNeeded(name, value)); + } + + public void RemovePropertyValueList(string name, IEnumerable valueList) + { + if (entries.ContainsKey(name)) + entries[name].RemoveValueList(valueList); + } + + // name should be either release or debug + public static XCBuildConfigurationData Create(string name) + { + var res = new XCBuildConfigurationData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "XCBuildConfiguration"); + res.SetPropertyString("name", name); + return res; + } + + public override void UpdateProps() + { + SetPropertyString("baseConfigurationReference", baseConfigurationReference); + + var dict = m_Properties.CreateDict("buildSettings"); + foreach (var kv in entries) + { + if (kv.Value.val.Count == 0) + continue; + else if (kv.Value.val.Count == 1) + dict.SetString(kv.Key, kv.Value.val[0]); + else // kv.Value.val.Count > 1 + { + var array = dict.CreateArray(kv.Key); + foreach (var value in kv.Value.val) + array.AddString(value); + } + } + } + public override void UpdateVars() + { + baseConfigurationReference = GetPropertyString("baseConfigurationReference"); + + entries = new SortedDictionary(); + if (m_Properties.Contains("buildSettings")) + { + var dict = m_Properties["buildSettings"].AsDict(); + foreach (var key in dict.values.Keys) + { + var value = dict[key]; + if (value is PBXElementString) + { + if (entries.ContainsKey(key)) + entries[key].val.Add(value.AsString()); + else + entries.Add(key, BuildConfigEntryData.FromNameValue(key, value.AsString())); + } + else if (value is PBXElementArray) + { + foreach (var pvalue in value.AsArray().values) + { + if (pvalue is PBXElementString) + { + if (entries.ContainsKey(key)) + entries[key].val.Add(pvalue.AsString()); + else + entries.Add(key, BuildConfigEntryData.FromNameValue(key, pvalue.AsString())); + } + } + } + } + } + } + } + + internal class PBXContainerItemProxyData : PBXObjectData + { + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "containerPortal/*" + }); + + internal override PropertyCommentChecker checker { get { return checkerData; } } + + public static PBXContainerItemProxyData Create(string containerRef, string proxyType, + string remoteGlobalGUID, string remoteInfo) + { + var res = new PBXContainerItemProxyData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXContainerItemProxy"); + res.SetPropertyString("containerPortal", containerRef); // guid + res.SetPropertyString("proxyType", proxyType); + res.SetPropertyString("remoteGlobalIDString", remoteGlobalGUID); // guid + res.SetPropertyString("remoteInfo", remoteInfo); + return res; + } + } + + internal class PBXReferenceProxyData : PBXObjectData + { + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "remoteRef/*" + }); + + internal override PropertyCommentChecker checker { get { return checkerData; } } + + public string path { get { return GetPropertyString("path"); } } + + public static PBXReferenceProxyData Create(string path, string fileType, + string remoteRef, string sourceTree) + { + var res = new PBXReferenceProxyData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXReferenceProxy"); + res.SetPropertyString("path", path); + res.SetPropertyString("fileType", fileType); + res.SetPropertyString("remoteRef", remoteRef); + res.SetPropertyString("sourceTree", sourceTree); + return res; + } + } + + internal class PBXTargetDependencyData : PBXObjectData + { + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "target/*", + "targetProxy/*" + }); + + internal override PropertyCommentChecker checker { get { return checkerData; } } + + public static PBXTargetDependencyData Create(string target, string targetProxy) + { + var res = new PBXTargetDependencyData(); + res.guid = PBXGUID.Generate(); + res.SetPropertyString("isa", "PBXTargetDependency"); + res.SetPropertyString("target", target); + res.SetPropertyString("targetProxy", targetProxy); + return res; + } + } + + internal class ProjectReference + { + public string group; // guid + public string projectRef; // guid + + public static ProjectReference Create(string group, string projectRef) + { + var res = new ProjectReference(); + res.group = group; + res.projectRef = projectRef; + return res; + } + } + + internal class PBXProjectObjectData : PBXObjectData + { + private static PropertyCommentChecker checkerData = new PropertyCommentChecker(new string[]{ + "buildConfigurationList/*", + "mainGroup/*", + "projectReferences/*/ProductGroup/*", + "projectReferences/*/ProjectRef/*", + "targets/*" + }); + + internal override PropertyCommentChecker checker { get { return checkerData; } } + + public List projectReferences = new List(); + public string mainGroup { get { return GetPropertyString("mainGroup"); } } + public List targets = new List(); + public List knownAssetTags = new List(); + public string buildConfigList; + // the name of the entitlements file required for some capabilities. + public string entitlementsFile; + public List capabilities = new List(); + public Dictionary teamIDs = new Dictionary(); + + + public void AddReference(string productGroup, string projectRef) + { + projectReferences.Add(ProjectReference.Create(productGroup, projectRef)); + } + + public override void UpdateProps() + { + m_Properties.values.Remove("projectReferences"); + if (projectReferences.Count > 0) + { + var array = m_Properties.CreateArray("projectReferences"); + foreach (var value in projectReferences) + { + var dict = array.AddDict(); + dict.SetString("ProductGroup", value.group); + dict.SetString("ProjectRef", value.projectRef); + } + }; + SetPropertyList("targets", targets); + SetPropertyString("buildConfigurationList", buildConfigList); + if (knownAssetTags.Count > 0) + { + PBXElementDict attrs; + if (m_Properties.Contains("attributes")) + attrs = m_Properties["attributes"].AsDict(); + else + attrs = m_Properties.CreateDict("attributes"); + var tags = attrs.CreateArray("knownAssetTags"); + foreach (var tag in knownAssetTags) + tags.AddString(tag); + } + + // Enable the capabilities. + foreach (var cap in capabilities) + { + var attrs = m_Properties.Contains("attributes") ? m_Properties["attributes"].AsDict() : m_Properties.CreateDict("attributes"); + var targAttr = attrs.Contains("TargetAttributes") ? attrs["TargetAttributes"].AsDict() : attrs.CreateDict("TargetAttributes"); + var target = targAttr.Contains(cap.targetGuid) ? targAttr[cap.targetGuid].AsDict() : targAttr.CreateDict(cap.targetGuid); + var sysCap = target.Contains("SystemCapabilities") ? target["SystemCapabilities"].AsDict() : target.CreateDict("SystemCapabilities"); + + var capabilityId = cap.capability.id; + var currentCapability = sysCap.Contains(capabilityId) ? sysCap[capabilityId].AsDict() : sysCap.CreateDict(capabilityId); + currentCapability.SetString("enabled", "1"); + } + + // Set the team id + foreach (KeyValuePair teamID in teamIDs) + { + var attrs = m_Properties.Contains("attributes") ? m_Properties["attributes"].AsDict() : m_Properties.CreateDict("attributes"); + var targAttr = attrs.Contains("TargetAttributes") ? attrs["TargetAttributes"].AsDict() : attrs.CreateDict("TargetAttributes"); + var target = targAttr.Contains(teamID.Key) ? targAttr[teamID.Key].AsDict() : targAttr.CreateDict(teamID.Key); + target.SetString("DevelopmentTeam", teamID.Value); + } + } + + public override void UpdateVars() + { + projectReferences = new List(); + if (m_Properties.Contains("projectReferences")) + { + var el = m_Properties["projectReferences"].AsArray(); + foreach (var value in el.values) + { + PBXElementDict dict = value.AsDict(); + if (dict.Contains("ProductGroup") && dict.Contains("ProjectRef")) + { + string group = dict["ProductGroup"].AsString(); + string projectRef = dict["ProjectRef"].AsString(); + projectReferences.Add(ProjectReference.Create(group, projectRef)); + } + } + } + targets = GetPropertyList("targets"); + buildConfigList = GetPropertyString("buildConfigurationList"); + + // update knownAssetTags + knownAssetTags = new List(); + if (m_Properties.Contains("attributes")) + { + var el = m_Properties["attributes"].AsDict(); + if (el.Contains("knownAssetTags")) + { + var tags = el["knownAssetTags"].AsArray(); + foreach (var tag in tags.values) + knownAssetTags.Add(tag.AsString()); + } + + capabilities = new List(); + teamIDs = new Dictionary(); + + if (el.Contains("TargetAttributes")) + { + var targetAttr = el["TargetAttributes"].AsDict(); + foreach (var attr in targetAttr.values) + { + if (attr.Key == "DevelopmentTeam") + { + teamIDs.Add(attr.Key, attr.Value.AsString()); + } + + if (attr.Key == "SystemCapabilities") + { + var caps = el["SystemCapabilities"].AsDict(); + foreach (var cap in caps.values) + capabilities.Add(new PBXCapabilityType.TargetCapabilityPair(attr.Key, PBXCapabilityType.StringToPBXCapabilityType(cap.Value.AsString()))); + } + } + } + } + } + } + +} // namespace UnityEditor.iOS.Xcode + diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Objects.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Objects.cs.meta new file mode 100644 index 0000000..69ebfd1 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Objects.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 67cf10cf994604c719f706525399f867 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Parser.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Parser.cs new file mode 100644 index 0000000..7f1a1bb --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Parser.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.IO; +using System.Linq; +using System; + + +namespace ChillyRoom.UnityEditor.iOS.Xcode.PBX +{ + + class ValueAST {} + + // IdentifierAST := \ + class IdentifierAST : ValueAST + { + public int value = 0; // token id + } + + // TreeAST := '{' KeyValuePairList '}' + // KeyValuePairList := KeyValuePair ',' KeyValuePairList + // KeyValuePair ',' + // (empty) + class TreeAST : ValueAST + { + public List values = new List(); + } + + // ListAST := '(' ValueList ')' + // ValueList := ValueAST ',' ValueList + // ValueAST ',' + // (empty) + class ArrayAST : ValueAST + { + public List values = new List(); + } + + // KeyValueAST := IdentifierAST '=' ValueAST ';' + // ValueAST := IdentifierAST | TreeAST | ListAST + class KeyValueAST + { + public IdentifierAST key = null; + public ValueAST value = null; // either IdentifierAST, TreeAST or ListAST + } + + class Parser + { + TokenList tokens; + int currPos; + + public Parser(TokenList tokens) + { + this.tokens = tokens; + currPos = SkipComments(0); + } + + int SkipComments(int pos) + { + while (pos < tokens.Count && tokens[pos].type == TokenType.Comment) + { + pos++; + } + return pos; + } + + // returns new position + int IncInternal(int pos) + { + if (pos >= tokens.Count) + return pos; + pos++; + + return SkipComments(pos); + } + + // Increments current pointer if not past the end, returns previous pos + int Inc() + { + int prev = currPos; + currPos = IncInternal(currPos); + return prev; + } + + // Returns the token type of the current token + TokenType Tok() + { + if (currPos >= tokens.Count) + return TokenType.EOF; + return tokens[currPos].type; + } + + void SkipIf(TokenType type) + { + if (Tok() == type) + Inc(); + } + + string GetErrorMsg() + { + return "Invalid PBX project (parsing line " + tokens[currPos].line + ")"; + } + + public IdentifierAST ParseIdentifier() + { + if (Tok() != TokenType.String && Tok() != TokenType.QuotedString) + throw new Exception(GetErrorMsg()); + var ast = new IdentifierAST(); + ast.value = Inc(); + return ast; + } + + public TreeAST ParseTree() + { + if (Tok() != TokenType.LBrace) + throw new Exception(GetErrorMsg()); + Inc(); + + var ast = new TreeAST(); + while (Tok() != TokenType.RBrace && Tok() != TokenType.EOF) + { + ast.values.Add(ParseKeyValue()); + } + SkipIf(TokenType.RBrace); + return ast; + } + + public ArrayAST ParseList() + { + if (Tok() != TokenType.LParen) + throw new Exception(GetErrorMsg()); + Inc(); + + var ast = new ArrayAST(); + while (Tok() != TokenType.RParen && Tok() != TokenType.EOF) + { + ast.values.Add(ParseValue()); + SkipIf(TokenType.Comma); + } + SkipIf(TokenType.RParen); + return ast; + } + + // throws on error + public KeyValueAST ParseKeyValue() + { + var ast = new KeyValueAST(); + ast.key = ParseIdentifier(); + + if (Tok() != TokenType.Eq) + throw new Exception(GetErrorMsg()); + Inc(); // skip '=' + + ast.value = ParseValue(); + SkipIf(TokenType.Semicolon); + + return ast; + } + + // throws on error + public ValueAST ParseValue() + { + if (Tok() == TokenType.String || Tok() == TokenType.QuotedString) + return ParseIdentifier(); + else if (Tok() == TokenType.LBrace) + return ParseTree(); + else if (Tok() == TokenType.LParen) + return ParseList(); + throw new Exception(GetErrorMsg()); + } + } + +} // namespace UnityEditor.iOS.Xcode \ No newline at end of file diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Parser.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Parser.cs.meta new file mode 100644 index 0000000..115422c --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Parser.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5ce5a244bc4e04a668ab097db4456665 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Sections.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Sections.cs new file mode 100644 index 0000000..20ddcc3 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Sections.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; + +// Basr classes for section handling + +namespace ChillyRoom.UnityEditor.iOS.Xcode.PBX +{ + + // common base + internal abstract class SectionBase + { + public abstract void AddObject(string key, PBXElementDict value); + public abstract void WriteSection(StringBuilder sb, GUIDToCommentMap comments); + } + + // known section: contains objects that we care about + internal class KnownSectionBase : SectionBase where T : PBXObjectData, new() + { + private Dictionary m_Entries = new Dictionary(); + + private string m_Name; + + public KnownSectionBase(string sectionName) + { + m_Name = sectionName; + } + + public IEnumerable> GetEntries() + { + return m_Entries; + } + + public IEnumerable GetGuids() + { + return m_Entries.Keys; + } + + public IEnumerable GetObjects() + { + return m_Entries.Values; + } + + public override void AddObject(string key, PBXElementDict value) + { + T obj = new T(); + obj.guid = key; + obj.SetPropertiesWhenSerializing(value); + obj.UpdateVars(); + m_Entries[obj.guid] = obj; + } + + public override void WriteSection(StringBuilder sb, GUIDToCommentMap comments) + { + if (m_Entries.Count == 0) + return; // do not write empty sections + + sb.AppendFormat("\n\n/* Begin {0} section */", m_Name); + var keys = new List(m_Entries.Keys); + keys.Sort(StringComparer.Ordinal); + foreach (string key in keys) + { + T obj = m_Entries[key]; + obj.UpdateProps(); + sb.Append("\n\t\t"); + comments.WriteStringBuilder(sb, obj.guid); + sb.Append(" = "); + Serializer.WriteDict(sb, obj.GetPropertiesWhenSerializing(), 2, + obj.shouldCompact, obj.checker, comments); + sb.Append(";"); + } + sb.AppendFormat("\n/* End {0} section */", m_Name); + } + + // returns null if not found + public T this[string guid] + { + get { + if (m_Entries.ContainsKey(guid)) + return m_Entries[guid]; + return null; + } + } + + public bool HasEntry(string guid) + { + return m_Entries.ContainsKey(guid); + } + + public void AddEntry(T obj) + { + m_Entries[obj.guid] = obj; + } + + public void RemoveEntry(string guid) + { + if (m_Entries.ContainsKey(guid)) + m_Entries.Remove(guid); + } + } + + // we assume there is only one PBXProject entry + internal class PBXProjectSection : KnownSectionBase + { + public PBXProjectSection() : base("PBXProject") + { + } + + public PBXProjectObjectData project + { + get { + foreach (var kv in GetEntries()) + return kv.Value; + return null; + } + } + } + +} // UnityEditor.iOS.Xcode diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Sections.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Sections.cs.meta new file mode 100644 index 0000000..aadf820 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Sections.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 60639b138b83f4ce9b2448c38a6ba285 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Serializer.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Serializer.cs new file mode 100644 index 0000000..df0ea39 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Serializer.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; +using System.Linq; + +namespace ChillyRoom.UnityEditor.iOS.Xcode.PBX +{ + class PropertyCommentChecker + { + private int m_Level; + private bool m_All; + private List> m_Props; + + /* The argument is an array of matcher strings each of which determine + whether a property with a certain path needs to be decorated with a + comment. + + A path is a number of path elements concatenated by '/'. The last path + element is either: + the key if we're referring to a dict key + the value if we're referring to a value in an array + the value if we're referring to a value in a dict + All other path elements are either: + the key if the container is dict + '*' if the container is array + + Path matcher has the same structure as a path, except that any of the + elements may be '*'. Matcher matches a path if both have the same number + of elements and for each pair matcher element is the same as path element + or is '*'. + + a/b/c matches a/b/c but not a/b nor a/b/c/d + a/* /c matches a/d/c but not a/b nor a/b/c/d + * /* /* matches any path from three elements + */ + protected PropertyCommentChecker(int level, List> props) + { + m_Level = level; + m_All = false; + m_Props = props; + } + + public PropertyCommentChecker() + { + m_Level = 0; + m_All = false; + m_Props = new List>(); + } + + public PropertyCommentChecker(IEnumerable props) + { + m_Level = 0; + m_All = false; + m_Props = new List>(); + foreach (var prop in props) + { + m_Props.Add(new List(prop.Split('/'))); + } + } + + bool CheckContained(string prop) + { + if (m_All) + return true; + foreach (var list in m_Props) + { + if (list.Count == m_Level+1) + { + if (list[m_Level] == prop) + return true; + if (list[m_Level] == "*") + { + m_All = true; // short-circuit all at this level + return true; + } + } + } + return false; + } + + public bool CheckStringValueInArray(string value) { return CheckContained(value); } + public bool CheckKeyInDict(string key) { return CheckContained(key); } + + public bool CheckStringValueInDict(string key, string value) + { + foreach (var list in m_Props) + { + if (list.Count == m_Level + 2) + { + if ((list[m_Level] == "*" || list[m_Level] == key) && + list[m_Level+1] == "*" || list[m_Level+1] == value) + return true; + } + } + return false; + } + + public PropertyCommentChecker NextLevel(string prop) + { + var newList = new List>(); + foreach (var list in m_Props) + { + if (list.Count <= m_Level+1) + continue; + if (list[m_Level] == "*" || list[m_Level] == prop) + newList.Add(list); + } + return new PropertyCommentChecker(m_Level + 1, newList); + } + } + + class Serializer + { + public static PBXElementDict ParseTreeAST(TreeAST ast, TokenList tokens, string text) + { + var el = new PBXElementDict(); + foreach (var kv in ast.values) + { + PBXElementString key = ParseIdentifierAST(kv.key, tokens, text); + PBXElement value = ParseValueAST(kv.value, tokens, text); + el[key.value] = value; + } + return el; + } + + public static PBXElementArray ParseArrayAST(ArrayAST ast, TokenList tokens, string text) + { + var el = new PBXElementArray(); + foreach (var v in ast.values) + { + el.values.Add(ParseValueAST(v, tokens, text)); + } + return el; + } + + public static PBXElement ParseValueAST(ValueAST ast, TokenList tokens, string text) + { + if (ast is TreeAST) + return ParseTreeAST((TreeAST)ast, tokens, text); + if (ast is ArrayAST) + return ParseArrayAST((ArrayAST)ast, tokens, text); + if (ast is IdentifierAST) + return ParseIdentifierAST((IdentifierAST)ast, tokens, text); + return null; + } + + public static PBXElementString ParseIdentifierAST(IdentifierAST ast, TokenList tokens, string text) + { + Token tok = tokens[ast.value]; + string value; + switch (tok.type) + { + case TokenType.String: + value = text.Substring(tok.begin, tok.end - tok.begin); + return new PBXElementString(value); + case TokenType.QuotedString: + value = text.Substring(tok.begin, tok.end - tok.begin); + value = PBXStream.UnquoteString(value); + return new PBXElementString(value); + default: + throw new Exception("Internal parser error"); + } + } + + static string k_Indent = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; + + static string GetIndent(int indent) + { + return k_Indent.Substring(0, indent); + } + + static void WriteStringImpl(StringBuilder sb, string s, bool comment, GUIDToCommentMap comments) + { + if (comment) + comments.WriteStringBuilder(sb, s); + else + sb.Append(PBXStream.QuoteStringIfNeeded(s)); + } + + public static void WriteDictKeyValue(StringBuilder sb, string key, PBXElement value, int indent, bool compact, + PropertyCommentChecker checker, GUIDToCommentMap comments) + { + if (!compact) + { + sb.Append("\n"); + sb.Append(GetIndent(indent)); + } + WriteStringImpl(sb, key, checker.CheckKeyInDict(key), comments); + sb.Append(" = "); + + if (value is PBXElementString) + WriteStringImpl(sb, value.AsString(), checker.CheckStringValueInDict(key, value.AsString()), comments); + else if (value is PBXElementDict) + WriteDict(sb, value.AsDict(), indent, compact, checker.NextLevel(key), comments); + else if (value is PBXElementArray) + WriteArray(sb, value.AsArray(), indent, compact, checker.NextLevel(key), comments); + sb.Append(";"); + if (compact) + sb.Append(" "); + } + + public static void WriteDict(StringBuilder sb, PBXElementDict el, int indent, bool compact, + PropertyCommentChecker checker, GUIDToCommentMap comments) + { + sb.Append("{"); + + if (el.Contains("isa")) + WriteDictKeyValue(sb, "isa", el["isa"], indent+1, compact, checker, comments); + var keys = new List(el.values.Keys); + keys.Sort(StringComparer.Ordinal); + foreach (var key in keys) + { + if (key != "isa") + WriteDictKeyValue(sb, key, el[key], indent+1, compact, checker, comments); + } + if (!compact) + { + sb.Append("\n"); + sb.Append(GetIndent(indent)); + } + sb.Append("}"); + } + + public static void WriteArray(StringBuilder sb, PBXElementArray el, int indent, bool compact, + PropertyCommentChecker checker, GUIDToCommentMap comments) + { + sb.Append("("); + foreach (var value in el.values) + { + if (!compact) + { + sb.Append("\n"); + sb.Append(GetIndent(indent+1)); + } + + if (value is PBXElementString) + WriteStringImpl(sb, value.AsString(), checker.CheckStringValueInArray(value.AsString()), comments); + else if (value is PBXElementDict) + WriteDict(sb, value.AsDict(), indent+1, compact, checker.NextLevel("*"), comments); + else if (value is PBXElementArray) + WriteArray(sb, value.AsArray(), indent+1, compact, checker.NextLevel("*"), comments); + sb.Append(","); + if (compact) + sb.Append(" "); + } + + if (!compact) + { + sb.Append("\n"); + sb.Append(GetIndent(indent)); + } + sb.Append(")"); + } + } + +} // namespace UnityEditor.iOS.Xcode + diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Serializer.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Serializer.cs.meta new file mode 100644 index 0000000..43a2fe9 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Serializer.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: afe8316bf614347729fac3466d7c77f2 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Utils.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Utils.cs new file mode 100644 index 0000000..6753c10 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Utils.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; + +namespace ChillyRoom.UnityEditor.iOS.Xcode.PBX +{ + internal class GUIDToCommentMap + { + private Dictionary m_Dict = new Dictionary(); + + public string this[string guid] + { + get { + if (m_Dict.ContainsKey(guid)) + return m_Dict[guid]; + return null; + } + } + + public void Add(string guid, string comment) + { + if (m_Dict.ContainsKey(guid)) + return; + m_Dict.Add(guid, comment); + } + + public void Remove(string guid) + { + m_Dict.Remove(guid); + } + + public string Write(string guid) + { + string comment = this[guid]; + if (comment == null) + return guid; + return String.Format("{0} /* {1} */", guid, comment); + } + + public void WriteStringBuilder(StringBuilder sb, string guid) + { + string comment = this[guid]; + if (comment == null) + sb.Append(guid); + else + { + // {0} /* {1} */ + sb.Append(guid).Append(" /* ").Append(comment).Append(" */"); + } + } + } + + internal class PBXGUID + { + internal delegate string GuidGenerator(); + + // We allow changing Guid generator to make testing of PBXProject possible + private static GuidGenerator guidGenerator = DefaultGuidGenerator; + + internal static string DefaultGuidGenerator() + { + return Guid.NewGuid().ToString("N").Substring(8).ToUpper(); + } + + internal static void SetGuidGenerator(GuidGenerator generator) + { + guidGenerator = generator; + } + + // Generates a GUID. + public static string Generate() + { + return guidGenerator(); + } + } + + internal class PBXRegex + { + public static string GuidRegexString = "[A-Fa-f0-9]{24}"; + } + + internal class PBXStream + { + static bool DontNeedQuotes(string src) + { + // using a regex instead of explicit matching slows down common cases by 40% + if (src.Length == 0) + return false; + + bool hasSlash = false; + for (int i = 0; i < src.Length; ++i) + { + char c = src[i]; + if (Char.IsLetterOrDigit(c) || c == '.' || c == '*' || c == '_') + continue; + if (c == '/') + { + hasSlash = true; + continue; + } + return false; + } + if (hasSlash) + { + if (src.Contains("//") || src.Contains("/*") || src.Contains("*/")) + return false; + } + return true; + } + + // Quotes the given string if it contains special characters. Note: if the string already + // contains quotes, then they are escaped and the entire string quoted again + public static string QuoteStringIfNeeded(string src) + { + if (DontNeedQuotes(src)) + return src; + return "\"" + src.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n") + "\""; + } + + // If the given string is quoted, removes the quotes and unescapes any quotes within the string + public static string UnquoteString(string src) + { + if (!src.StartsWith("\"") || !src.EndsWith("\"")) + return src; + return src.Substring(1, src.Length - 2).Replace("\\\\", "\u569f").Replace("\\\"", "\"") + .Replace("\\n", "\n").Replace("\u569f", "\\"); // U+569f is a rarely used Chinese character + } + } + + internal enum PBXFileType + { + NotBuildable, + Framework, + Source, + Resource, + CopyFile + } + + internal class FileTypeUtils + { + internal class FileTypeDesc + { + public FileTypeDesc(string typeName, PBXFileType type) + { + this.name = typeName; + this.type = type; + this.isExplicit = false; + } + + public FileTypeDesc(string typeName, PBXFileType type, bool isExplicit) + { + this.name = typeName; + this.type = type; + this.isExplicit = isExplicit; + } + + public string name; + public PBXFileType type; + public bool isExplicit; + } + + private static readonly Dictionary types = + new Dictionary + { + { "a", new FileTypeDesc("archive.ar", PBXFileType.Framework) }, + { "app", new FileTypeDesc("wrapper.application", PBXFileType.NotBuildable, true) }, + { "appex", new FileTypeDesc("wrapper.app-extension", PBXFileType.CopyFile) }, + { "bin", new FileTypeDesc("archive.macbinary", PBXFileType.Resource) }, + { "s", new FileTypeDesc("sourcecode.asm", PBXFileType.Source) }, + { "c", new FileTypeDesc("sourcecode.c.c", PBXFileType.Source) }, + { "cc", new FileTypeDesc("sourcecode.cpp.cpp", PBXFileType.Source) }, + { "cpp", new FileTypeDesc("sourcecode.cpp.cpp", PBXFileType.Source) }, + { "swift", new FileTypeDesc("sourcecode.swift", PBXFileType.Source) }, + { "dll", new FileTypeDesc("file", PBXFileType.NotBuildable) }, + { "framework", new FileTypeDesc("wrapper.framework", PBXFileType.Framework) }, + { "h", new FileTypeDesc("sourcecode.c.h", PBXFileType.NotBuildable) }, + { "pch", new FileTypeDesc("sourcecode.c.h", PBXFileType.NotBuildable) }, + { "icns", new FileTypeDesc("image.icns", PBXFileType.Resource) }, + { "xcassets", new FileTypeDesc("folder.assetcatalog", PBXFileType.Resource) }, + { "inc", new FileTypeDesc("sourcecode.inc", PBXFileType.NotBuildable) }, + { "m", new FileTypeDesc("sourcecode.c.objc", PBXFileType.Source) }, + { "mm", new FileTypeDesc("sourcecode.cpp.objcpp", PBXFileType.Source ) }, + { "nib", new FileTypeDesc("wrapper.nib", PBXFileType.Resource) }, + { "plist", new FileTypeDesc("text.plist.xml", PBXFileType.Resource) }, + { "png", new FileTypeDesc("image.png", PBXFileType.Resource) }, + { "rtf", new FileTypeDesc("text.rtf", PBXFileType.Resource) }, + { "tiff", new FileTypeDesc("image.tiff", PBXFileType.Resource) }, + { "txt", new FileTypeDesc("text", PBXFileType.Resource) }, + { "json", new FileTypeDesc("text.json", PBXFileType.Resource) }, + { "xcodeproj", new FileTypeDesc("wrapper.pb-project", PBXFileType.NotBuildable) }, + { "xib", new FileTypeDesc("file.xib", PBXFileType.Resource) }, + { "strings", new FileTypeDesc("text.plist.strings", PBXFileType.Resource) }, + { "storyboard",new FileTypeDesc("file.storyboard", PBXFileType.Resource) }, + { "bundle", new FileTypeDesc("wrapper.plug-in", PBXFileType.Resource) }, + { "dylib", new FileTypeDesc("compiled.mach-o.dylib", PBXFileType.Framework) }, + { "tbd", new FileTypeDesc("sourcecode.text-based-dylib-definition", PBXFileType.Framework) } + }; + + public static string TrimExtension(string ext) + { + return ext.TrimStart('.'); + } + + public static bool IsKnownExtension(string ext) + { + ext = TrimExtension(ext); + return types.ContainsKey(ext); + } + + internal static bool IsFileTypeExplicit(string ext) + { + ext = TrimExtension(ext); + if (types.ContainsKey(ext)) + return types[ext].isExplicit; + return false; + } + + public static PBXFileType GetFileType(string ext, bool isFolderRef) + { + ext = TrimExtension(ext); + if (isFolderRef) + return PBXFileType.Resource; + if (!types.ContainsKey(ext)) + return PBXFileType.Resource; + return types[ext].type; + } + + public static string GetTypeName(string ext) + { + ext = TrimExtension(ext); + if (types.ContainsKey(ext)) + return types[ext].name; + // Xcode actually checks the file contents to determine the file type. + // Text files have "text" type and all other files have "file" type. + // Since we can't reasonably determine whether the file in question is + // a text file, we just take the safe route and return "file" type. + return "file"; + } + + public static bool IsBuildableFile(string ext) + { + ext = TrimExtension(ext); + if (!types.ContainsKey(ext)) + return true; + if (types[ext].type != PBXFileType.NotBuildable) + return true; + return false; + } + + public static bool IsBuildable(string ext, bool isFolderReference) + { + ext = TrimExtension(ext); + if (isFolderReference) + return true; + return IsBuildableFile(ext); + } + + private static readonly Dictionary sourceTree = new Dictionary + { + { PBXSourceTree.Absolute, "" }, + { PBXSourceTree.Group, "" }, + { PBXSourceTree.Build, "BUILT_PRODUCTS_DIR" }, + { PBXSourceTree.Developer, "DEVELOPER_DIR" }, + { PBXSourceTree.Sdk, "SDKROOT" }, + { PBXSourceTree.Source, "SOURCE_ROOT" }, + }; + + private static readonly Dictionary stringToSourceTreeMap = new Dictionary + { + { "", PBXSourceTree.Absolute }, + { "", PBXSourceTree.Group }, + { "BUILT_PRODUCTS_DIR", PBXSourceTree.Build }, + { "DEVELOPER_DIR", PBXSourceTree.Developer }, + { "SDKROOT", PBXSourceTree.Sdk }, + { "SOURCE_ROOT", PBXSourceTree.Source }, + }; + + internal static string SourceTreeDesc(PBXSourceTree tree) + { + return sourceTree[tree]; + } + + // returns PBXSourceTree.Source on error + internal static PBXSourceTree ParseSourceTree(string tree) + { + if (stringToSourceTreeMap.ContainsKey(tree)) + return stringToSourceTreeMap[tree]; + return PBXSourceTree.Source; + } + + internal static List AllAbsoluteSourceTrees() + { + return new List{PBXSourceTree.Absolute, PBXSourceTree.Build, + PBXSourceTree.Developer, PBXSourceTree.Sdk, PBXSourceTree.Source}; + } + } + +} // UnityEditor.iOS.Xcode diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Utils.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Utils.cs.meta new file mode 100644 index 0000000..0dec2b9 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBX/Utils.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 8eafbccf2ded14760af588ffcb1ca0f5 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXCapabilityType.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXCapabilityType.cs new file mode 100644 index 0000000..bf1382f --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXCapabilityType.cs @@ -0,0 +1,125 @@ +namespace ChillyRoom.UnityEditor.iOS.Xcode +{ + /// + /// List of all the capabilities available. + /// + public sealed class PBXCapabilityType + { + public static readonly PBXCapabilityType ApplePay = new PBXCapabilityType ("com.apple.ApplePay", true); + public static readonly PBXCapabilityType AppGroups = new PBXCapabilityType ("com.apple.ApplicationGroups.iOS", true); + public static readonly PBXCapabilityType AssociatedDomains = new PBXCapabilityType ("com.apple.SafariKeychain", true); + public static readonly PBXCapabilityType BackgroundModes = new PBXCapabilityType ("com.apple.BackgroundModes", false); + public static readonly PBXCapabilityType DataProtection = new PBXCapabilityType ("com.apple.DataProtection", true); + public static readonly PBXCapabilityType GameCenter = new PBXCapabilityType ("com.apple.GameCenter", false, "GameKit.framework"); + public static readonly PBXCapabilityType HealthKit = new PBXCapabilityType ("com.apple.HealthKit", true, "HealthKit.framework"); + public static readonly PBXCapabilityType HomeKit = new PBXCapabilityType ("com.apple.HomeKit", true, "HomeKit.framework"); + public static readonly PBXCapabilityType iCloud = new PBXCapabilityType("com.apple.iCloud", true, "CloudKit.framework", true); + public static readonly PBXCapabilityType InAppPurchase = new PBXCapabilityType ("com.apple.InAppPurchase", false); + public static readonly PBXCapabilityType InterAppAudio = new PBXCapabilityType ("com.apple.InterAppAudio", true, "AudioToolbox.framework"); + public static readonly PBXCapabilityType KeychainSharing = new PBXCapabilityType ("com.apple.KeychainSharing", true); + public static readonly PBXCapabilityType Maps = new PBXCapabilityType("com.apple.Maps.iOS", false, "MapKit.framework"); + public static readonly PBXCapabilityType PersonalVPN = new PBXCapabilityType("com.apple.VPNLite", true, "NetworkExtension.framework"); + public static readonly PBXCapabilityType PushNotifications = new PBXCapabilityType ("com.apple.Push", true); + public static readonly PBXCapabilityType Siri = new PBXCapabilityType ("com.apple.Siri", true); + public static readonly PBXCapabilityType Wallet = new PBXCapabilityType ("com.apple.Wallet", true, "PassKit.framework"); + public static readonly PBXCapabilityType WirelessAccessoryConfiguration = new PBXCapabilityType("com.apple.WAC", true, "ExternalAccessory.framework"); + + private readonly string m_ID; + private readonly bool m_RequiresEntitlements; + private readonly string m_Framework; + private readonly bool m_OptionalFramework; + + public bool optionalFramework + { + get { return m_OptionalFramework; } + } + + public string framework + { + get { return m_Framework; } + } + + public string id + { + get { return m_ID; } + } + + public bool requiresEntitlements + { + get { return m_RequiresEntitlements; } + } + + public struct TargetCapabilityPair + { + public string targetGuid; + public PBXCapabilityType capability; + + public TargetCapabilityPair(string guid, PBXCapabilityType type) + { + targetGuid = guid; + capability = type; + } + } + + /// + /// This private object represents what a capability changes in the PBXProject file + /// + /// The string used in the PBXProject file to identify the capability and mark it as enabled. + /// This capability requires an entitlements file therefore we need to add this entitlements file to the code signing entitlement. + /// Specify which framework need to be added to the project for this capability, if "" no framework are added. + /// Some capability (right now only iCloud) adds a framework, not all the time but just when some option are checked + /// this parameter indicates if one of them is checked. + private PBXCapabilityType(string _id, bool _requiresEntitlements, string _framework = "", bool _optionalFramework = false) + { + m_ID = _id; + m_RequiresEntitlements = _requiresEntitlements; + m_Framework = _framework; + m_OptionalFramework = _optionalFramework; + } + + public static PBXCapabilityType StringToPBXCapabilityType(string cap) + { + switch (cap) + { + case "com.apple.ApplePay": + return ApplePay; + case "com.apple.ApplicationGroups.iOS": + return AppGroups; + case "com.apple.SafariKeychain": + return AssociatedDomains; + case "com.apple.BackgroundModes": + return BackgroundModes; + case "com.apple.DataProtection": + return DataProtection; + case "com.apple.GameCenter": + return GameCenter; + case "com.apple.HealthKit": + return HealthKit; + case "com.apple.HomeKit": + return HomeKit; + case "com.apple.iCloud": + return iCloud; + case "com.apple.InAppPurchase": + return InAppPurchase; + case "com.apple.InterAppAudio": + return InterAppAudio; + case "com.apple.KeychainSharing": + return KeychainSharing; + case "com.apple.Maps.iOS": + return Maps; + case "com.apple.VPNLite": + return PersonalVPN; + case "com.apple.Push": + return PushNotifications; + case "com.apple.Siri": + return Siri; + case "com.apple.Wallet": + return Wallet; + case "WAC": + return WirelessAccessoryConfiguration; + default: + return null; + } + } + } +} diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXCapabilityType.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXCapabilityType.cs.meta new file mode 100644 index 0000000..a9fdc80 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXCapabilityType.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 71f2dd6273cda4741b9b3bac41a9b765 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXPath.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXPath.cs new file mode 100644 index 0000000..821c8bc --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXPath.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; + +namespace ChillyRoom.UnityEditor.iOS.Xcode +{ + internal class PBXPath + { + /// Replaces '\' with '/'. We need to apply this function to all paths that come from the user + /// of the API because we store paths to pbxproj and on windows we may get path with '\' slashes + /// instead of '/' slashes + public static string FixSlashes(string path) + { + if (path == null) + return null; + return path.Replace('\\', '/'); + } + + public static void Combine(string path1, PBXSourceTree tree1, string path2, PBXSourceTree tree2, + out string resPath, out PBXSourceTree resTree) + { + if (tree2 == PBXSourceTree.Group) + { + resPath = Combine(path1, path2); + resTree = tree1; + return; + } + + resPath = path2; + resTree = tree2; + } + + // Combines two paths + public static string Combine(string path1, string path2) + { + if (path2.StartsWith("/")) + return path2; + if (path1.EndsWith("/")) + return path1 + path2; + if (path1 == "") + return path2; + if (path2 == "") + return path1; + return path1 + "/" + path2; + } + + public static string GetDirectory(string path) + { + path = path.TrimEnd('/'); + int pos = path.LastIndexOf('/'); + if (pos == -1) + return ""; + else + return path.Substring(0, pos); + } + + public static string GetCurrentDirectory() + { + if (Environment.OSVersion.Platform != PlatformID.MacOSX && + Environment.OSVersion.Platform != PlatformID.Unix) + { + throw new Exception("PBX project compatible current directory can only obtained on OSX"); + } + + string path = Directory.GetCurrentDirectory(); + path = FixSlashes(path); + if (!IsPathRooted(path)) + return "/" + path; + return path; + } + + public static string GetFilename(string path) + { + int pos = path.LastIndexOf('/'); + if (pos == -1) + return path; + else + return path.Substring(pos + 1); + } + + public static bool IsPathRooted(string path) + { + if (path == null || path.Length == 0) + return false; + return path[0] == '/'; + } + + public static string GetFullPath(string path) + { + if (IsPathRooted(path)) + return path; + else + return Combine(GetCurrentDirectory(), path); + } + + public static string[] Split(string path) + { + if (string.IsNullOrEmpty(path)) + return new string[]{}; + return path.Split(new[]{'/'}, StringSplitOptions.RemoveEmptyEntries); + } + } + +} // UnityEditor.iOS.Xcode diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXPath.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXPath.cs.meta new file mode 100644 index 0000000..cba89f0 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXPath.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 57be5de7dcd844631b3e835186497c25 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProject.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProject.cs new file mode 100644 index 0000000..7fb04b1 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProject.cs @@ -0,0 +1,1538 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using UnityEngine; +using ChillyRoom.UnityEditor.iOS.Xcode.PBX; + +namespace ChillyRoom.UnityEditor.iOS.Xcode +{ + using PBXBuildFileSection = KnownSectionBase; + using PBXFileReferenceSection = KnownSectionBase; + using PBXGroupSection = KnownSectionBase; + using PBXContainerItemProxySection = KnownSectionBase; + using PBXReferenceProxySection = KnownSectionBase; + using PBXSourcesBuildPhaseSection = KnownSectionBase; + using PBXFrameworksBuildPhaseSection= KnownSectionBase; + using PBXResourcesBuildPhaseSection = KnownSectionBase; + using PBXCopyFilesBuildPhaseSection = KnownSectionBase; + using PBXShellScriptBuildPhaseSection = KnownSectionBase; + using PBXVariantGroupSection = KnownSectionBase; + using PBXNativeTargetSection = KnownSectionBase; + using PBXTargetDependencySection = KnownSectionBase; + using XCBuildConfigurationSection = KnownSectionBase; + using XCConfigurationListSection = KnownSectionBase; + using UnknownSection = KnownSectionBase; + + // Determines the tree the given path is relative to + public enum PBXSourceTree + { + Absolute, // The path is absolute + Source, // The path is relative to the source folder + Group, // The path is relative to the folder it's in. This enum is used only internally, + // do not use it as function parameter + Build, // The path is relative to the build products folder + Developer, // The path is relative to the developer folder + Sdk // The path is relative to the sdk folder + } + + public class PBXProject + { + PBXProjectData m_Data = new PBXProjectData(); + + // convenience accessors for public members of data. This is temporary; will be fixed by an interface change + // of PBXProjectData + internal PBXContainerItemProxySection containerItems { get { return m_Data.containerItems; } } + internal PBXReferenceProxySection references { get { return m_Data.references; } } + internal PBXSourcesBuildPhaseSection sources { get { return m_Data.sources; } } + internal PBXFrameworksBuildPhaseSection frameworks { get { return m_Data.frameworks; } } + internal PBXResourcesBuildPhaseSection resources { get { return m_Data.resources; } } + internal PBXCopyFilesBuildPhaseSection copyFiles { get { return m_Data.copyFiles; } } + internal PBXShellScriptBuildPhaseSection shellScripts { get { return m_Data.shellScripts; } } + internal PBXNativeTargetSection nativeTargets { get { return m_Data.nativeTargets; } } + internal PBXTargetDependencySection targetDependencies { get { return m_Data.targetDependencies; } } + internal PBXVariantGroupSection variantGroups { get { return m_Data.variantGroups; } } + internal XCBuildConfigurationSection buildConfigs { get { return m_Data.buildConfigs; } } + internal XCConfigurationListSection buildConfigLists { get { return m_Data.buildConfigLists; } } + internal PBXProjectSection project { get { return m_Data.project; } } + + internal PBXBuildFileData BuildFilesGet(string guid) { return m_Data.BuildFilesGet(guid); } + internal void BuildFilesAdd(string targetGuid, PBXBuildFileData buildFile) { m_Data.BuildFilesAdd(targetGuid, buildFile); } + internal void BuildFilesRemove(string targetGuid, string fileGuid) { m_Data.BuildFilesRemove(targetGuid, fileGuid); } + internal PBXBuildFileData BuildFilesGetForSourceFile(string targetGuid, string fileGuid) { return m_Data.BuildFilesGetForSourceFile(targetGuid, fileGuid); } + internal IEnumerable BuildFilesGetAll() { return m_Data.BuildFilesGetAll(); } + internal void FileRefsAdd(string realPath, string projectPath, PBXGroupData parent, PBXFileReferenceData fileRef) { m_Data.FileRefsAdd(realPath, projectPath, parent, fileRef); } + internal PBXFileReferenceData FileRefsGet(string guid) { return m_Data.FileRefsGet(guid); } + internal PBXFileReferenceData FileRefsGetByRealPath(string path, PBXSourceTree sourceTree) { return m_Data.FileRefsGetByRealPath(path, sourceTree); } + internal PBXFileReferenceData FileRefsGetByProjectPath(string path) { return m_Data.FileRefsGetByProjectPath(path); } + internal void FileRefsRemove(string guid) { m_Data.FileRefsRemove(guid); } + internal PBXGroupData GroupsGet(string guid) { return m_Data.GroupsGet(guid); } + internal PBXGroupData GroupsGetByChild(string childGuid) { return m_Data.GroupsGetByChild(childGuid); } + internal PBXGroupData GroupsGetMainGroup() { return m_Data.GroupsGetMainGroup(); } + internal PBXGroupData GroupsGetByProjectPath(string sourceGroup) { return m_Data.GroupsGetByProjectPath(sourceGroup); } + internal void GroupsAdd(string projectPath, PBXGroupData parent, PBXGroupData gr) { m_Data.GroupsAdd(projectPath, parent, gr); } + internal void GroupsAddDuplicate(PBXGroupData gr) { m_Data.GroupsAddDuplicate(gr); } + internal void GroupsRemove(string guid) { m_Data.GroupsRemove(guid); } + PBXGroupData GroupsGetByName (string name) { return m_Data.GroupsGetByName (name); } + internal FileGUIDListBase BuildSectionAny(PBXNativeTargetData target, string path, bool isFolderRef) { return m_Data.BuildSectionAny(target, path, isFolderRef); } + internal FileGUIDListBase BuildSectionAny(string sectionGuid) { return m_Data.BuildSectionAny(sectionGuid); } + + /// + /// Returns the path to PBX project in the given Unity build path. This function can only + /// be used in Unity-generated projects + /// + /// The project build path + /// The path to the PBX project file that can later be opened via ReadFromFile function + public static string GetPBXProjectPath(string buildPath) + { + return PBXPath.Combine(buildPath, "Unity-iPhone.xcodeproj/project.pbxproj"); + } + + /// + /// Returns the default main target name in Unity project. + /// The returned target name can then be used to retrieve the GUID of the target via TargetGuidByName + /// function. This function can only be used in Unity-generated projects. + /// + /// The default main target name. + public static string GetUnityTargetName() + { + return "Unity-iPhone"; + } + + /// + /// Returns the default test target name in Unity project. + /// The returned target name can then be used to retrieve the GUID of the target via TargetGuidByName + /// function. This function can only be used in Unity-generated projects. + /// + /// The default test target name. + public static string GetUnityTestTargetName() + { + return "Unity-iPhone Tests"; + } + + /// + /// Returns the GUID of the project. The project GUID identifies a project-wide native target which + /// is used to set project-wide properties. This GUID can be passed to any functions that accepts + /// target GUIDs as parameters. + /// + /// The GUID of the project. + public string ProjectGuid() + { + return project.project.guid; + } + + /// + /// Returns the GUID of the native target with the given name. + /// In projects produced by Unity the main target can be retrieved via GetUnityTargetName function, + /// whereas the test target name can be retrieved by GetUnityTestTargetName function. + /// + /// The name of the native target. + /// The GUID identifying the native target. + public string TargetGuidByName(string name) + { + foreach (var entry in nativeTargets.GetEntries()) + if (entry.Value.name == name) + return entry.Key; + return null; + } + + /// + /// Checks if files with the given extension are known to PBXProject. + /// + /// Returns true if is the extension is known, false otherwise. + /// The file extension (leading dot is not necessary, but accepted). + public static bool IsKnownExtension(string ext) + { + return FileTypeUtils.IsKnownExtension(ext); + } + + /// + /// Checks if files with the given extension are known to PBXProject. + /// Returns true if the extension is not known by PBXProject. + /// + /// Returns true if is the extension is known, false otherwise. + /// The file extension (leading dot is not necessary, but accepted). + public static bool IsBuildable(string ext) + { + return FileTypeUtils.IsBuildableFile(ext); + } + + // The same file can be referred to by more than one project path. + private string AddFileImpl(string path, string projectPath, PBXSourceTree tree, bool isFolderReference) + { + path = PBXPath.FixSlashes(path); + projectPath = PBXPath.FixSlashes(projectPath); + + if (!isFolderReference && Path.GetExtension(path) != Path.GetExtension(projectPath)) + throw new Exception("Project and real path extensions do not match"); + + string guid = FindFileGuidByProjectPath(projectPath); + if (guid == null) + guid = FindFileGuidByRealPath(path); + if (guid == null) + { + PBXFileReferenceData fileRef; + if (isFolderReference) + fileRef = PBXFileReferenceData.CreateFromFolderReference(path, PBXPath.GetFilename(projectPath), tree); + else + fileRef = PBXFileReferenceData.CreateFromFile(path, PBXPath.GetFilename(projectPath), tree); + PBXGroupData parent = CreateSourceGroup(PBXPath.GetDirectory(projectPath)); + parent.children.AddGUID(fileRef.guid); + FileRefsAdd(path, projectPath, parent, fileRef); + guid = fileRef.guid; + } + return guid; + } + + /// + /// Adds a new file reference to the list of known files. + /// The group structure is automatically created to correspond to the project path. + /// To add the file to the list of files to build, pass the returned value to [[AddFileToBuild]]. + /// + /// The GUID of the added file. It can later be used to add the file for building to targets, etc. + /// The physical path to the file on the filesystem. + /// The project path to the file. + /// The source tree the path is relative to. By default it's [[PBXSourceTree.Source]]. + /// The [[PBXSourceTree.Group]] tree is not supported. + public string AddFile(string path, string projectPath, PBXSourceTree sourceTree = PBXSourceTree.Source) + { + if (sourceTree == PBXSourceTree.Group) + throw new Exception("sourceTree must not be PBXSourceTree.Group"); + return AddFileImpl(path, projectPath, sourceTree, false); + } + + /// + /// Adds a new folder reference to the list of known files. + /// The group structure is automatically created to correspond to the project path. + /// To add the folder reference to the list of files to build, pass the returned value to [[AddFileToBuild]]. + /// + /// The GUID of the added folder reference. It can later be used to add the file for building to targets, etc. + /// The physical path to the folder on the filesystem. + /// The project path to the folder. + /// The source tree the path is relative to. By default it's [[PBXSourceTree.Source]]. + /// The [[PBXSourceTree.Group]] tree is not supported. + public string AddFolderReference(string path, string projectPath, PBXSourceTree sourceTree = PBXSourceTree.Source) + { + if (sourceTree == PBXSourceTree.Group) + throw new Exception("sourceTree must not be PBXSourceTree.Group"); + return AddFileImpl(path, projectPath, sourceTree, true); + } + + private void AddBuildFileImpl(string targetGuid, string fileGuid, bool weak, string compileFlags) + { + PBXNativeTargetData target = nativeTargets[targetGuid]; + PBXFileReferenceData fileRef = FileRefsGet(fileGuid); + + string ext = Path.GetExtension(fileRef.path); + + if (FileTypeUtils.IsBuildable(ext, fileRef.isFolderReference) && + BuildFilesGetForSourceFile(targetGuid, fileGuid) == null) + { + PBXBuildFileData buildFile = PBXBuildFileData.CreateFromFile(fileGuid, weak, compileFlags); + BuildFilesAdd(targetGuid, buildFile); + BuildSectionAny(target, ext, fileRef.isFolderReference).files.AddGUID(buildFile.guid); + } + } + + /// + /// Configures file for building for the given native target. + /// A projects containing multiple native targets, a single file or folder reference can be + /// configured to be built in all, some or none of the targets. The file or folder reference is + /// added to appropriate build section depending on the file extension. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The file guid returned by [[AddFile]] or [[AddFolderReference]]. + public void AddFileToBuild(string targetGuid, string fileGuid) + { + AddBuildFileImpl(targetGuid, fileGuid, false, null); + } + + /// + /// Configures file for building for the given native target with specific compiler flags. + /// The function is equivalent to [[AddFileToBuild()]] except that compile flags are specified. + /// A projects containing multiple native targets, a single file or folder reference can be + /// configured to be built in all, some or none of the targets. The file or folder reference is + /// added to appropriate build section depending on the file extension. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The file guid returned by [[AddFile]] or [[AddFolderReference]]. + /// Compile flags to use. + public void AddFileToBuildWithFlags(string targetGuid, string fileGuid, string compileFlags) + { + AddBuildFileImpl(targetGuid, fileGuid, false, compileFlags); + } + + /// + /// Configures file for building for the given native target on specific build section. + /// The function is equivalent to [[AddFileToBuild()]] except that specific build section is specified. + /// A projects containing multiple native targets, a single file or folder reference can be + /// configured to be built in all, some or none of the targets. The file or folder reference is + /// added to appropriate build section depending on the file extension. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The GUID of the section to add the file to. + /// The file guid returned by [[AddFile]] or [[AddFolderReference]]. + public void AddFileToBuildSection(string targetGuid, string sectionGuid, string fileGuid) + { + PBXBuildFileData buildFile = PBXBuildFileData.CreateFromFile(fileGuid, false, null); + BuildFilesAdd(targetGuid, buildFile); + BuildSectionAny(sectionGuid).files.AddGUID(buildFile.guid); + } + + /// + /// Returns compile flags set for the specific file. + /// Null is returned if the file has no configured compile flags or the file is not configured for + /// building on the given target. + /// + /// The compile flags for the specified file. + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The GUID of the file. + public List GetCompileFlagsForFile(string targetGuid, string fileGuid) + { + var buildFile = BuildFilesGetForSourceFile(targetGuid, fileGuid); + if (buildFile == null) + return null; + if (buildFile.compileFlags == null) + return new List(); + return new List(buildFile.compileFlags.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries)); + } + + /// + /// Sets the compilation flags for the given file in the given target. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The GUID of the file. + /// The list of compile flags or null if the flags should be unset. + public void SetCompileFlagsForFile(string targetGuid, string fileGuid, List compileFlags) + { + var buildFile = BuildFilesGetForSourceFile(targetGuid, fileGuid); + if (buildFile == null) + return; + if (compileFlags == null) + buildFile.compileFlags = null; + else + buildFile.compileFlags = string.Join(" ", compileFlags.ToArray()); + } + + /// + /// Adds an asset tag for the given file. + /// The asset tags identify resources that will be downloaded via On Demand Resources functionality. + /// A request for specific tag will initiate download of all files, configured for that tag. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The GUID of the file. + /// The name of the asset tag. + public void AddAssetTagForFile(string targetGuid, string fileGuid, string tag) + { + var buildFile = BuildFilesGetForSourceFile(targetGuid, fileGuid); + if (buildFile == null) + return; + if (!buildFile.assetTags.Contains(tag)) + buildFile.assetTags.Add(tag); + if (!project.project.knownAssetTags.Contains(tag)) + project.project.knownAssetTags.Add(tag); + } + + /// + /// Removes an asset tag for the given file. + /// The function does nothing if the file is not configured for building in the given target or if + /// the asset tag is not present in the list of asset tags configured for file. If the file was the + /// last file referring to the given tag across the Xcode project, then the tag is removed from the + /// list of known tags. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The GUID of the file. + /// The name of the asset tag. + public void RemoveAssetTagForFile(string targetGuid, string fileGuid, string tag) + { + var buildFile = BuildFilesGetForSourceFile(targetGuid, fileGuid); + if (buildFile == null) + return; + buildFile.assetTags.Remove(tag); + // remove from known tags if this was the last one + foreach (var buildFile2 in BuildFilesGetAll()) + { + if (buildFile2.assetTags.Contains(tag)) + return; + } + project.project.knownAssetTags.Remove(tag); + } + + /// + /// Adds the asset tag to the list of tags to download during initial installation. + /// The function does nothing if there are no files that use the given asset tag across the project. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the asset tag. + public void AddAssetTagToDefaultInstall(string targetGuid, string tag) + { + if (!project.project.knownAssetTags.Contains(tag)) + return; + AddBuildProperty(targetGuid, "ON_DEMAND_RESOURCES_INITIAL_INSTALL_TAGS", tag); + } + + /// + /// Removes the asset tag from the list of tags to download during initial installation. + /// The function does nothing if the tag is not already configured for downloading during + /// initial installation. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the asset tag. + public void RemoveAssetTagFromDefaultInstall(string targetGuid, string tag) + { + UpdateBuildProperty(targetGuid, "ON_DEMAND_RESOURCES_INITIAL_INSTALL_TAGS", null, new[]{tag}); + } + + /// + /// Removes an asset tag. + /// Removes the given asset tag from the list of configured asset tags for all files on all targets, + /// the list of asset tags configured for initial installation and the list of known asset tags in + /// the Xcode project. + /// + /// The name of the asset tag. + public void RemoveAssetTag(string tag) + { + foreach (var buildFile in BuildFilesGetAll()) + buildFile.assetTags.Remove(tag); + foreach (var targetGuid in nativeTargets.GetGuids()) + RemoveAssetTagFromDefaultInstall(targetGuid, tag); + project.project.knownAssetTags.Remove(tag); + } + + /// + /// Checks if the project contains a file with the given physical path. + /// The search is performed across all absolute source trees. + /// + /// Returns true if the project contains the file, false otherwise. + /// The physical path of the file. + public bool ContainsFileByRealPath(string path) + { + return FindFileGuidByRealPath(path) != null; + } + + /// + /// Checks if the project contains a file with the given physical path. + /// + /// Returns true if the project contains the file, false otherwise. + /// The physical path of the file. + /// The source tree path is relative to. The [[PBXSourceTree.Group]] tree is not supported. + public bool ContainsFileByRealPath(string path, PBXSourceTree sourceTree) + { + if (sourceTree == PBXSourceTree.Group) + throw new Exception("sourceTree must not be PBXSourceTree.Group"); + return FindFileGuidByRealPath(path, sourceTree) != null; + } + + /// + /// Checks if the project contains a file with the given project path. + /// + /// Returns true if the project contains the file, false otherwise. + /// The project path of the file. + public bool ContainsFileByProjectPath(string path) + { + return FindFileGuidByProjectPath(path) != null; + } + + /// + /// Checks whether the given system framework is a dependency of a target. + /// The function assumes system frameworks are located in System/Library/Frameworks folder in the SDK source tree. + /// + /// Returns true if the given framework is a dependency of the given target, + /// false otherwise. + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the framework. The extension of the filename must be ".framework". + public bool ContainsFramework(string targetGuid, string framework) + { + var fileGuid = FindFileGuidByRealPath("System/Library/Frameworks/" + framework, PBXSourceTree.Sdk); + if (fileGuid == null) + return false; + + var buildFile = BuildFilesGetForSourceFile(targetGuid, fileGuid); + return (buildFile != null); + } + + /// + /// Adds a system framework dependency for the specified target. + /// The function assumes system frameworks are located in System/Library/Frameworks folder in the SDK source tree. + /// The framework is added to Frameworks logical folder in the project. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the framework. The extension of the filename must be ".framework". + /// true if the framework is optional (i.e. weakly linked) required, + /// false if the framework is required. + public void AddFrameworkToProject(string targetGuid, string framework, bool weak) + { + string fileGuid = AddFile("System/Library/Frameworks/" + framework, "Frameworks/" + framework, PBXSourceTree.Sdk); + AddBuildFileImpl(targetGuid, fileGuid, weak, null); + } + + /// + /// Removes a system framework dependency for the specified target. + /// The function assumes system frameworks are located in System/Library/Frameworks folder in the SDK source tree. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the framework. The extension of the filename must be ".framework". + public void RemoveFrameworkFromProject(string targetGuid, string framework) + { + var fileGuid = FindFileGuidByRealPath("System/Library/Frameworks/" + framework, PBXSourceTree.Sdk); + if (fileGuid == null) + return; + + BuildFilesRemove(targetGuid, fileGuid); + } + + // Allow user to add a Capability + public bool AddCapability(string targetGuid, PBXCapabilityType capability, string entitlementsFilePath = null, bool addOptionalFramework = false) + { + // If the capability requires entitlements then you have to provide the name of it or we don't add the capability. + if (capability.requiresEntitlements && entitlementsFilePath == "") + { + throw new Exception("Couldn't add the Xcode Capability " + capability.id + " to the PBXProject file because this capability requires an entitlement file."); + } + var p = project.project; + + // If an entitlement with a different name was added for another capability + // we don't add this capacity. + if (p.entitlementsFile != null && entitlementsFilePath != null && p.entitlementsFile != entitlementsFilePath) + { + if (p.capabilities.Count > 0){ + //throw new WarningException("Attention, it seems that you have multiple entitlements file. Only one will be added the Project : " + p.entitlementsFile); + throw new Exception("Attention, it seems that you have multiple entitlements file. Only one will be added the Project : " + p.entitlementsFile); + } + + return false; + } + + // Add the capability only if it doesn't already exist. + if (p.capabilities.Contains(new PBXCapabilityType.TargetCapabilityPair(targetGuid, capability))) + { + //throw new WarningException("This capability has already been added. Method ignored"); + } + + p.capabilities.Add(new PBXCapabilityType.TargetCapabilityPair(targetGuid, capability)); + + // Add the required framework. + if (capability.framework != "" && !capability.optionalFramework || + (capability.framework != "" && capability.optionalFramework && addOptionalFramework)) + { + AddFrameworkToProject(targetGuid, capability.framework, false); + } + + // Finally add the entitlement code signing if it wasn't added before. + if (entitlementsFilePath != null && p.entitlementsFile == null) + { + p.entitlementsFile = entitlementsFilePath; + AddFileImpl(entitlementsFilePath, entitlementsFilePath, PBXSourceTree.Source, false); + SetBuildProperty(targetGuid, "CODE_SIGN_ENTITLEMENTS", PBXPath.FixSlashes(entitlementsFilePath)); + } + return true; + } + + // The Xcode project needs a team set to be able to complete code signing or to add some capabilities. + public void SetTeamId(string targetGuid, string teamId) + { + SetBuildProperty(targetGuid, "DEVELOPMENT_TEAM", teamId); + project.project.teamIDs.Add(targetGuid, teamId); + } + + /// + /// Finds a file with the given physical path in the project, if any. + /// + /// The GUID of the file if the search succeeded, null otherwise. + /// The physical path of the file. + /// The source tree path is relative to. The [[PBXSourceTree.Group]] tree is not supported. + public string FindFileGuidByRealPath(string path, PBXSourceTree sourceTree) + { + if (sourceTree == PBXSourceTree.Group) + throw new Exception("sourceTree must not be PBXSourceTree.Group"); + path = PBXPath.FixSlashes(path); + var fileRef = FileRefsGetByRealPath(path, sourceTree); + if (fileRef != null) + return fileRef.guid; + return null; + } + + /// + /// Finds a file with the given physical path in the project, if any. + /// The search is performed across all absolute source trees. + /// + /// The GUID of the file if the search succeeded, null otherwise. + /// The physical path of the file. + public string FindFileGuidByRealPath(string path) + { + path = PBXPath.FixSlashes(path); + + foreach (var tree in FileTypeUtils.AllAbsoluteSourceTrees()) + { + string res = FindFileGuidByRealPath(path, tree); + if (res != null) + return res; + } + return null; + } + + /// + /// Finds a file with the given project path in the project, if any. + /// + /// The GUID of the file if the search succeeded, null otherwise. + /// The project path of the file. + public string FindFileGuidByProjectPath(string path) + { + path = PBXPath.FixSlashes(path); + var fileRef = FileRefsGetByProjectPath(path); + if (fileRef != null) + return fileRef.guid; + return null; + } + + /// + /// Removes given file from the list of files to build for the given target. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The GUID of the file or folder reference. + public void RemoveFileFromBuild(string targetGuid, string fileGuid) + { + var buildFile = BuildFilesGetForSourceFile(targetGuid, fileGuid); + if (buildFile == null) + return; + BuildFilesRemove(targetGuid, fileGuid); + + string buildGuid = buildFile.guid; + if (buildGuid != null) + { + foreach (var section in sources.GetEntries()) + section.Value.files.RemoveGUID(buildGuid); + foreach (var section in resources.GetEntries()) + section.Value.files.RemoveGUID(buildGuid); + foreach (var section in copyFiles.GetEntries()) + section.Value.files.RemoveGUID(buildGuid); + foreach (var section in frameworks.GetEntries()) + section.Value.files.RemoveGUID(buildGuid); + } + } + + /// + /// Removes the given file from project. + /// The file is removed from the list of files to build for each native target and also removed + /// from the list of known files. + /// + /// The GUID of the file or folder reference. + public void RemoveFile(string fileGuid) + { + if (fileGuid == null) + return; + + // remove from parent + PBXGroupData parent = GroupsGetByChild(fileGuid); + if (parent != null) + parent.children.RemoveGUID(fileGuid); + RemoveGroupIfEmpty(parent); + + // remove actual file + foreach (var target in nativeTargets.GetEntries()) + RemoveFileFromBuild(target.Value.guid, fileGuid); + FileRefsRemove(fileGuid); + } + + void RemoveGroupIfEmpty(PBXGroupData gr) + { + if (gr.children.Count == 0 && gr != GroupsGetMainGroup()) + { + // remove from parent + PBXGroupData parent = GroupsGetByChild(gr.guid); + parent.children.RemoveGUID(gr.guid); + RemoveGroupIfEmpty(parent); + + // remove actual group + GroupsRemove(gr.guid); + } + } + + private void RemoveGroupChildrenRecursive(PBXGroupData parent) + { + List children = new List(parent.children); + parent.children.Clear(); + foreach (string guid in children) + { + PBXFileReferenceData file = FileRefsGet(guid); + if (file != null) + { + foreach (var target in nativeTargets.GetEntries()) + RemoveFileFromBuild(target.Value.guid, guid); + FileRefsRemove(guid); + continue; + } + + PBXGroupData gr = GroupsGet(guid); + if (gr != null) + { + RemoveGroupChildrenRecursive(gr); + GroupsRemove(gr.guid); + continue; + } + } + } + + internal void RemoveFilesByProjectPathRecursive(string projectPath) + { + projectPath = PBXPath.FixSlashes(projectPath); + PBXGroupData gr = GroupsGetByProjectPath(projectPath); + if (gr == null) + return; + RemoveGroupChildrenRecursive(gr); + RemoveGroupIfEmpty(gr); + } + + // Returns null on error + internal List GetGroupChildrenFiles(string projectPath) + { + projectPath = PBXPath.FixSlashes(projectPath); + PBXGroupData gr = GroupsGetByProjectPath(projectPath); + if (gr == null) + return null; + var res = new List(); + foreach (var guid in gr.children) + { + PBXFileReferenceData fileRef = FileRefsGet(guid); + if (fileRef != null) + res.Add(fileRef.name); + } + return res; + } + + // Returns an empty dictionary if no group or files are found + internal HashSet GetGroupChildrenFilesRefs(string projectPath) + { + projectPath = PBXPath.FixSlashes(projectPath); + PBXGroupData gr = GroupsGetByProjectPath(projectPath); + if (gr == null) + return new HashSet(); + HashSet res = new HashSet(); + foreach (var guid in gr.children) + { + PBXFileReferenceData fileRef = FileRefsGet(guid); + if (fileRef != null) + res.Add(fileRef.path); + } + return res == null ? new HashSet () : res; + } + + internal HashSet GetFileRefsByProjectPaths(IEnumerable paths) + { + HashSet ret = new HashSet(); + foreach (string path in paths) + { + string fixedPath = PBXPath.FixSlashes(path); + var fileRef = FileRefsGetByProjectPath(fixedPath); + if (fileRef != null) + ret.Add(fileRef.path); + } + return ret; + } + + private PBXGroupData GetPBXGroupChildByName(PBXGroupData group, string name) + { + foreach (string guid in group.children) + { + var gr = GroupsGet(guid); + if (gr != null && gr.name == name) + return gr; + } + return null; + } + + /// Creates source group identified by sourceGroup, if needed, and returns it. + /// If sourceGroup is empty or null, root group is returned + private PBXGroupData CreateSourceGroup(string sourceGroup) + { + sourceGroup = PBXPath.FixSlashes(sourceGroup); + + if (sourceGroup == null || sourceGroup == "") + return GroupsGetMainGroup(); + + PBXGroupData gr = GroupsGetByProjectPath(sourceGroup); + if (gr != null) + return gr; + + // the group does not exist -- create new + gr = GroupsGetMainGroup(); + + var elements = PBXPath.Split(sourceGroup); + string projectPath = null; + foreach (string pathEl in elements) + { + if (projectPath == null) + projectPath = pathEl; + else + projectPath += "/" + pathEl; + + PBXGroupData child = GetPBXGroupChildByName(gr, pathEl); + if (child != null) + gr = child; + else + { + PBXGroupData newGroup = PBXGroupData.Create(pathEl, pathEl, PBXSourceTree.Group); + gr.children.AddGUID(newGroup.guid); + GroupsAdd(projectPath, gr, newGroup); + gr = newGroup; + } + } + return gr; + } + + /// + /// Adds an external project dependency to the project. + /// + /// The path to the external Xcode project (the .xcodeproj file). + /// The project path to the new project. + /// The source tree the path is relative to. The [[PBXSourceTree.Group]] tree is not supported. + internal void AddExternalProjectDependency(string path, string projectPath, PBXSourceTree sourceTree) + { + if (sourceTree == PBXSourceTree.Group) + throw new Exception("sourceTree must not be PBXSourceTree.Group"); + path = PBXPath.FixSlashes(path); + projectPath = PBXPath.FixSlashes(projectPath); + + // note: we are duplicating products group for the project reference. Otherwise Xcode crashes. + PBXGroupData productGroup = PBXGroupData.CreateRelative("Products"); + GroupsAddDuplicate(productGroup); // don't use GroupsAdd here + + PBXFileReferenceData fileRef = PBXFileReferenceData.CreateFromFile(path, Path.GetFileName(projectPath), + sourceTree); + FileRefsAdd(path, projectPath, null, fileRef); + CreateSourceGroup(PBXPath.GetDirectory(projectPath)).children.AddGUID(fileRef.guid); + + project.project.AddReference(productGroup.guid, fileRef.guid); + } + + /** This function must be called only after the project the library is in has + been added as a dependency via AddExternalProjectDependency. projectPath must be + the same as the 'path' parameter passed to the AddExternalProjectDependency. + remoteFileGuid must be the guid of the referenced file as specified in + PBXFileReference section of the external project + + TODO: what. is remoteInfo entry in PBXContainerItemProxy? Is in referenced project name or + referenced library name without extension? + */ + internal void AddExternalLibraryDependency(string targetGuid, string filename, string remoteFileGuid, string projectPath, + string remoteInfo) + { + PBXNativeTargetData target = nativeTargets[targetGuid]; + filename = PBXPath.FixSlashes(filename); + projectPath = PBXPath.FixSlashes(projectPath); + + // find the products group to put the new library in + string projectGuid = FindFileGuidByRealPath(projectPath); + if (projectGuid == null) + throw new Exception("No such project"); + + string productsGroupGuid = null; + foreach (var proj in project.project.projectReferences) + { + if (proj.projectRef == projectGuid) + { + productsGroupGuid = proj.group; + break; + } + } + + if (productsGroupGuid == null) + throw new Exception("Malformed project: no project in project references"); + + PBXGroupData productGroup = GroupsGet(productsGroupGuid); + + // verify file extension + string ext = Path.GetExtension(filename); + if (!FileTypeUtils.IsBuildableFile(ext)) + throw new Exception("Wrong file extension"); + + // create ContainerItemProxy object + var container = PBXContainerItemProxyData.Create(projectGuid, "2", remoteFileGuid, remoteInfo); + containerItems.AddEntry(container); + + // create a reference and build file for the library + string typeName = FileTypeUtils.GetTypeName(ext); + + var libRef = PBXReferenceProxyData.Create(filename, typeName, container.guid, "BUILT_PRODUCTS_DIR"); + references.AddEntry(libRef); + PBXBuildFileData libBuildFile = PBXBuildFileData.CreateFromFile(libRef.guid, false, null); + BuildFilesAdd(targetGuid, libBuildFile); + BuildSectionAny(target, ext, false).files.AddGUID(libBuildFile.guid); + + // add to products folder + productGroup.children.AddGUID(libRef.guid); + } + + /// + /// Creates a new native target. + /// Target-specific build configurations are automatically created for each known build configuration name. + /// Note, that this is a requirement that follows from the structure of Xcode projects, not an implementation + /// detail of this function. The function creates a product file reference in the "Products" project folder + /// which refers to the target artifact that is built via this target. + /// + /// The GUID of the new target. + /// The name of the new target. + /// The file extension of the target artifact (leading dot is not necessary, but accepted). + /// The type of the target. For example: + /// "com.apple.product-type.app-extension" - App extension, + /// "com.apple.product-type.application.watchapp2" - WatchKit 2 application + public string AddTarget(string name, string ext, string type) + { + var buildConfigList = XCConfigurationListData.Create(); + buildConfigLists.AddEntry(buildConfigList); + + // create build file reference + string fullName = name + "." + FileTypeUtils.TrimExtension(ext); + var productFileRef = AddFile(fullName, "Products/" + fullName, PBXSourceTree.Build); + var newTarget = PBXNativeTargetData.Create(name, productFileRef, type, buildConfigList.guid); + nativeTargets.AddEntry(newTarget); + project.project.targets.Add(newTarget.guid); + + foreach (var buildConfigName in BuildConfigNames()) + AddBuildConfigForTarget(newTarget.guid, buildConfigName); + + return newTarget.guid; + } + + private IEnumerable GetAllTargetGuids() + { + var targets = new List(); + + targets.Add(project.project.guid); + targets.AddRange(nativeTargets.GetGuids()); + + return targets; + } + + /// + /// Returns the file reference of the artifact created by building target. + /// + /// The file reference of the artifact created by building target. + /// The GUID of the target as returned by [[TargetGuidByName()]]. + public string GetTargetProductFileRef(string targetGuid) + { + return nativeTargets[targetGuid].productReference; + } + + /// + /// Sets up a dependency between two targets. + /// + /// The GUID of the target that is depending on the dependency. + /// The GUID of the dependency target + internal void AddTargetDependency(string targetGuid, string targetDependencyGuid) + { + string dependencyName = nativeTargets[targetDependencyGuid].name; + var containerProxy = PBXContainerItemProxyData.Create(project.project.guid, "1", targetDependencyGuid, dependencyName); + containerItems.AddEntry(containerProxy); + + var targetDependency = PBXTargetDependencyData.Create(targetDependencyGuid, containerProxy.guid); + targetDependencies.AddEntry(targetDependency); + + nativeTargets[targetGuid].dependencies.AddGUID(targetDependency.guid); + } + + // Returns the GUID of the new configuration + // targetGuid can be either native target or the project target. + private string AddBuildConfigForTarget(string targetGuid, string name) + { + if (BuildConfigByName(targetGuid, name) != null) + { + throw new Exception(String.Format("A build configuration by name {0} already exists for target {1}", + targetGuid, name)); + } + var buildConfig = XCBuildConfigurationData.Create(name); + buildConfigs.AddEntry(buildConfig); + + buildConfigLists[GetConfigListForTarget(targetGuid)].buildConfigs.AddGUID(buildConfig.guid); + return buildConfig.guid; + } + + private void RemoveBuildConfigForTarget(string targetGuid, string name) + { + var buildConfigGuid = BuildConfigByName(targetGuid, name); + if (buildConfigGuid == null) + return; + buildConfigs.RemoveEntry(buildConfigGuid); + buildConfigLists[GetConfigListForTarget(targetGuid)].buildConfigs.RemoveGUID(buildConfigGuid); + } + + /// + /// Returns the GUID of build configuration with the given name for the specific target. + /// Null is returned if such configuration does not exist. + /// + /// The GUID of the build configuration or null if it does not exist. + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the build configuration. + public string BuildConfigByName(string targetGuid, string name) + { + foreach (string guid in buildConfigLists[GetConfigListForTarget(targetGuid)].buildConfigs) + { + var buildConfig = buildConfigs[guid]; + if (buildConfig != null && buildConfig.name == name) + return buildConfig.guid; + } + return null; + } + + /// + /// Returns the names of the build configurations available in the project. + /// The number and names of the build configurations is a project-wide setting. Each target has the + /// same number of build configurations and the names of these build configurations is the same. + /// In other words, [[BuildConfigByName()]] will succeed for all targets in the project and all + /// build configuration names returned by this function. + /// + /// An array of build config names. + public IEnumerable BuildConfigNames() + { + var names = new List(); + // We use the project target to fetch the build configs + foreach (var guid in buildConfigLists[project.project.buildConfigList].buildConfigs) + names.Add(buildConfigs[guid].name); + + return names; + } + + /// + /// Creates a new set of build configurations for all targets in the project. + /// The number and names of the build configurations is a project-wide setting. Each target has the + /// same number of build configurations and the names of these build configurations is the same. + /// The created configurations are initially empty. Care must be taken to fill them with reasonable + /// defaults. + /// The function throws an exception if a build configuration with the given name already exists. + /// + /// The name of the build configuration. + public void AddBuildConfig(string name) + { + foreach (var targetGuid in GetAllTargetGuids()) + AddBuildConfigForTarget(targetGuid, name); + } + + /// + /// Removes all build configurations with the given name from all targets in the project. + /// The number and names of the build configurations is a project-wide setting. Each target has the + /// same number of build configurations and the names of these build configurations is the same. + /// The function does nothing if the build configuration with the specified name does not exist. + /// + /// The name of the build configuration. + public void RemoveBuildConfig(string name) + { + foreach (var targetGuid in GetAllTargetGuids()) + RemoveBuildConfigForTarget(targetGuid, name); + } + + /// + /// Creates a new sources build phase for given target. + /// The new phase is placed at the end of the list of build phases configured for the target. + /// + /// Returns the GUID of the new phase. + /// The GUID of the target as returned by [[TargetGuidByName()]]. + public string AddSourcesBuildPhase(string targetGuid) + { + var phase = PBXSourcesBuildPhaseData.Create(); + sources.AddEntry(phase); + nativeTargets[targetGuid].phases.AddGUID(phase.guid); + return phase.guid; + } + + /// + /// Creates a new resources build phase for given target. + /// The new phase is placed at the end of the list of build phases configured for the target. + /// + /// Returns the GUID of the new phase. + /// The GUID of the target as returned by [[TargetGuidByName()]]. + public string AddResourcesBuildPhase(string targetGuid) + { + var phase = PBXResourcesBuildPhaseData.Create(); + resources.AddEntry(phase); + nativeTargets[targetGuid].phases.AddGUID(phase.guid); + return phase.guid; + } + + /// + /// Creates a new frameworks build phase for given target. + /// The new phase is placed at the end of the list of build phases configured for the target. + /// + /// Returns the GUID of the new phase. + /// The GUID of the target as returned by [[TargetGuidByName()]]. + public string AddFrameworksBuildPhase(string targetGuid) + { + var phase = PBXFrameworksBuildPhaseData.Create(); + frameworks.AddEntry(phase); + nativeTargets[targetGuid].phases.AddGUID(phase.guid); + return phase.guid; + } + + /// + /// Creates a new copy files build phase for given target. + /// The new phase is placed at the end of the list of build phases configured for the target. + /// + /// Returns the GUID of the new phase. + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the phase. + /// The destination path. + /// The "subfolder spec". The following usages are known: + /// - "13" for embedding app extension content + /// - "16" for embedding watch content + public string AddCopyFilesBuildPhase(string targetGuid, string name, string dstPath, string subfolderSpec) + { + var phase = PBXCopyFilesBuildPhaseData.Create(name, dstPath, subfolderSpec); + copyFiles.AddEntry(phase); + nativeTargets[targetGuid].phases.AddGUID(phase.guid); + return phase.guid; + } + + internal string GetConfigListForTarget(string targetGuid) + { + if (targetGuid == project.project.guid) + return project.project.buildConfigList; + else + return nativeTargets[targetGuid].buildConfigList; + } + + // Sets the baseConfigurationReference key for a XCBuildConfiguration. + // If the argument is null, the base configuration is removed. + internal void SetBaseReferenceForConfig(string configGuid, string baseReference) + { + buildConfigs[configGuid].baseConfigurationReference = baseReference; + } + + /// + /// Adds a value to build property list in all build configurations for the specified target. + /// Duplicate build properties are ignored. Values for names "LIBRARY_SEARCH_PATHS" and + /// "FRAMEWORK_SEARCH_PATHS" are quoted if they contain spaces. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the build property. + /// The value of the build property. + public void AddBuildProperty(string targetGuid, string name, string value) + { + foreach (string guid in buildConfigLists[GetConfigListForTarget(targetGuid)].buildConfigs) + AddBuildPropertyForConfig(guid, name, value); + } + + /// + /// Adds a value to build property list in all build configurations for the specified targets. + /// Duplicate build properties are ignored. Values for names "LIBRARY_SEARCH_PATHS" and + /// "FRAMEWORK_SEARCH_PATHS" are quoted if they contain spaces. + /// + /// The GUIDs of the target as returned by [[TargetGuidByName()]]. + /// The name of the build property. + /// The value of the build property. + public void AddBuildProperty(IEnumerable targetGuids, string name, string value) + { + foreach (string t in targetGuids) + AddBuildProperty(t, name, value); + } + + /// + /// Adds a value to build property list of the given build configuration + /// Duplicate build properties are ignored. Values for names "LIBRARY_SEARCH_PATHS" and + /// "FRAMEWORK_SEARCH_PATHS" are quoted if they contain spaces. + /// + /// The GUID of the build configuration as returned by [[BuildConfigByName()]]. + /// The name of the build property. + /// The value of the build property. + public void AddBuildPropertyForConfig(string configGuid, string name, string value) + { + buildConfigs[configGuid].AddProperty(name, value); + } + + /// + /// Adds a value to build property list of the given build configurations + /// Duplicate build properties are ignored. Values for names "LIBRARY_SEARCH_PATHS" and + /// "FRAMEWORK_SEARCH_PATHS" are quoted if they contain spaces. + /// + /// The GUIDs of the build configurations as returned by [[BuildConfigByName()]]. + /// The name of the build property. + /// The value of the build property. + public void AddBuildPropertyForConfig(IEnumerable configGuids, string name, string value) + { + foreach (string guid in configGuids) + AddBuildPropertyForConfig(guid, name, value); + } + + /// + /// Adds a value to build property list in all build configurations for the specified target. + /// Duplicate build properties are ignored. Values for names "LIBRARY_SEARCH_PATHS" and + /// "FRAMEWORK_SEARCH_PATHS" are quoted if they contain spaces. + /// + /// The GUID of the target as returned by [[TargetGuidByName()]]. + /// The name of the build property. + /// The value of the build property. + public void SetBuildProperty(string targetGuid, string name, string value) + { + foreach (string guid in buildConfigLists[GetConfigListForTarget(targetGuid)].buildConfigs) + SetBuildPropertyForConfig(guid, name, value); + } + + /// + /// Adds a value to build property list in all build configurations for the specified targets. + /// Duplicate build properties are ignored. Values for names "LIBRARY_SEARCH_PATHS" and + /// "FRAMEWORK_SEARCH_PATHS" are quoted if they contain spaces. + /// + /// The GUIDs of the target as returned by [[TargetGuidByName()]]. + /// The name of the build property. + /// The value of the build property. + public void SetBuildProperty(IEnumerable targetGuids, string name, string value) + { + foreach (string t in targetGuids) + SetBuildProperty(t, name, value); + } + public void SetBuildPropertyForConfig(string configGuid, string name, string value) + { + buildConfigs[configGuid].SetProperty(name, value); + } + public void SetBuildPropertyForConfig(IEnumerable configGuids, string name, string value) + { + foreach (string guid in configGuids) + SetBuildPropertyForConfig(guid, name, value); + } + + internal void RemoveBuildProperty(string targetGuid, string name) + { + foreach (string guid in buildConfigLists[GetConfigListForTarget(targetGuid)].buildConfigs) + RemoveBuildPropertyForConfig(guid, name); + } + internal void RemoveBuildProperty(IEnumerable targetGuids, string name) + { + foreach (string t in targetGuids) + RemoveBuildProperty(t, name); + } + internal void RemoveBuildPropertyForConfig(string configGuid, string name) + { + buildConfigs[configGuid].RemoveProperty(name); + } + internal void RemoveBuildPropertyForConfig(IEnumerable configGuids, string name) + { + foreach (string guid in configGuids) + RemoveBuildPropertyForConfig(guid, name); + } + + internal void RemoveBuildPropertyValueList(string targetGuid, string name, IEnumerable valueList) + { + foreach (string guid in buildConfigLists[GetConfigListForTarget(targetGuid)].buildConfigs) + RemoveBuildPropertyValueListForConfig(guid, name, valueList); + } + internal void RemoveBuildPropertyValueList(IEnumerable targetGuids, string name, IEnumerable valueList) + { + foreach (string t in targetGuids) + RemoveBuildPropertyValueList(t, name, valueList); + } + internal void RemoveBuildPropertyValueListForConfig(string configGuid, string name, IEnumerable valueList) + { + buildConfigs[configGuid].RemovePropertyValueList(name, valueList); + } + internal void RemoveBuildPropertyValueListForConfig(IEnumerable configGuids, string name, IEnumerable valueList) + { + foreach (string guid in configGuids) + RemoveBuildPropertyValueListForConfig(guid, name, valueList); + } + + /// Interprets the value of the given property as a set of space-delimited strings, then + /// removes strings equal to items to removeValues and adds strings in addValues. + public void UpdateBuildProperty(string targetGuid, string name, + IEnumerable addValues, IEnumerable removeValues) + { + foreach (string guid in buildConfigLists[GetConfigListForTarget(targetGuid)].buildConfigs) + UpdateBuildPropertyForConfig(guid, name, addValues, removeValues); + } + public void UpdateBuildProperty(IEnumerable targetGuids, string name, + IEnumerable addValues, IEnumerable removeValues) + { + foreach (string t in targetGuids) + UpdateBuildProperty(t, name, addValues, removeValues); + } + public void UpdateBuildPropertyForConfig(string configGuid, string name, + IEnumerable addValues, IEnumerable removeValues) + { + var config = buildConfigs[configGuid]; + if (config != null) + { + if (removeValues != null) + foreach (var v in removeValues) + config.RemovePropertyValue(name, v); + if (addValues != null) + foreach (var v in addValues) + config.AddProperty(name, v); + } + } + public void UpdateBuildPropertyForConfig(IEnumerable configGuids, string name, + IEnumerable addValues, IEnumerable removeValues) + { + foreach (string guid in configGuids) + UpdateBuildProperty(guid, name, addValues, removeValues); + } + + internal string ShellScriptByName(string targetGuid, string name) + { + foreach (var phase in nativeTargets[targetGuid].phases) + { + var script = shellScripts[phase]; + if (script != null && script.name == name) + return script.guid; + } + return null; + } + + internal void AppendShellScriptBuildPhase(string targetGuid, string name, string shellPath, string shellScript) + { + PBXShellScriptBuildPhaseData shellScriptPhase = PBXShellScriptBuildPhaseData.Create(name, shellPath, shellScript); + + shellScripts.AddEntry(shellScriptPhase); + nativeTargets[targetGuid].phases.AddGUID(shellScriptPhase.guid); + } + + internal void AppendShellScriptBuildPhase(IEnumerable targetGuids, string name, string shellPath, string shellScript) + { + PBXShellScriptBuildPhaseData shellScriptPhase = PBXShellScriptBuildPhaseData.Create(name, shellPath, shellScript); + + shellScripts.AddEntry(shellScriptPhase); + foreach (string guid in targetGuids) + { + nativeTargets[guid].phases.AddGUID(shellScriptPhase.guid); + } + } + + public void ReadFromFile(string path) + { + ReadFromString(File.ReadAllText(path)); + } + + public void ReadFromString(string src) + { + TextReader sr = new StringReader(src); + ReadFromStream(sr); + } + + public void ReadFromStream(TextReader sr) + { + m_Data.ReadFromStream(sr); + } + + public void WriteToFile(string path) + { + File.WriteAllText(path, WriteToString()); + } + + public void WriteToStream(TextWriter sw) + { + sw.Write(WriteToString()); + } + + public string WriteToString() + { + return m_Data.WriteToString(); + } + + internal PBXProjectObjectData GetProjectInternal() + { + return project.project; + } + + /// + /// Add the reference to a locale .lproj to a VariantGroup. + /// + /// Name of the locale to use, such as zh-Hans, ja, de. Find the codes in XCode Localizations section + /// Name of the variant group for the localizable resources, such as InfoPlist.strings + /// path to the InfoPlist.strings file, relative to xcode project format. Make sure your *.strings files are copied over first + public void AddLocalization (string variantGroupName, string locale, string path) + { + path = PBXPath.FixSlashes(path); + + PBXVariantGroupData variantGroup = VariantGroupsGetByName (variantGroupName); + + if (variantGroup == null) + { + variantGroup = CreateLocalizableVariantGroup (variantGroupName); + } + else + { + // get guid of the build phase + string buildPhaseGuid = ResourceBuildPhaseByTargetName (GetUnityTargetName ()); + + PBXBuildFileData buildFileRef = BuildFilesGetForSourceFile(TargetGuidByName(GetUnityTargetName()), variantGroup.guid); + if(buildFileRef == null) + { + Debug.Log("adding variant to resource build"); + // add file to build target + string buildFileGuid = AddFileRefToBuild (TargetGuidByName (GetUnityTargetName ()), variantGroup.guid); + // add BuildFileRef to resouce build phase + AddFileToResourceBuildPhase (buildPhaseGuid, buildFileGuid); + } + } + + PBXFileReferenceData fileRef = FileRefsGetByRealPath(path, PBXSourceTree.Source); + if(fileRef == null) + { + Debug.Log("create new file reference: " + locale + ", path: " + path); + fileRef = PBXFileReferenceData.CreateFromFile (path, locale, PBXSourceTree.Source); + FileRefsAdd (path, path, variantGroup, fileRef); + } + + if (!variantGroup.children.Contains (fileRef.guid)) { + variantGroup.children.AddGUID (fileRef.guid); + } + } + + private void AddToGroup (string name, string guid) + { + var group = GroupsGetByName (name); + if (group != null) { + group.children.AddGUID (guid); + } + } + + private PBXVariantGroupData AddVariantGroup (string name, PBXSourceTree sourceTree) + { + PBXVariantGroupData variantGroup = VariantGroupsGetByName (name); + if (variantGroup == null) { + variantGroup = PBXVariantGroupData.Create (name, sourceTree); + m_Data.variantGroups.AddEntry (variantGroup); + } + return variantGroup; + } + + private PBXVariantGroupData VariantGroupsGetByName (string name) + { + foreach (var group in variantGroups.GetEntries ()) + if (group.Value.name == name) + return group.Value; + return null; + } + + /// + /// Creates a VariantGroup for localizable resources (e. g. Localizable.strings) + /// This VariantGroup is the container of all localizable resource files for each locale (.lproj) + /// + /// Variant group name. + /// The variant group the localizable resources. + private PBXVariantGroupData CreateLocalizableVariantGroup (string name) + { + // Create PBXVariantGroup + var variantGroup = AddVariantGroup (name, PBXSourceTree.Group); + // add PBXVariantGroup to CustomTemplate + AddToGroup ("CustomTemplate", variantGroup.guid); + // add PBXVariantGroup to PBXBuildFile + string buildFileGuid = AddFileRefToBuild (TargetGuidByName (GetUnityTargetName ()), variantGroup.guid); + // get guid of the build phase + string buildPhaseGuid = ResourceBuildPhaseByTargetName (GetUnityTargetName ()); + // add BuildFileRef to resouce build phase + AddFileToResourceBuildPhase (buildPhaseGuid, buildFileGuid); + return variantGroup; + } + + private void AddFileToResourceBuildPhase (string buildPhaseGuid, string fileGuid) + { + foreach (var entry in resources.GetEntries ()) { + if (entry.Value.guid == buildPhaseGuid) { + entry.Value.files.AddGUID (fileGuid); + } + } + } + + /// + /// Add a file reference to a specific target. + /// + /// The guid of the resource build phase. + /// Target name. + /// The file refernce + private string AddFileRefToBuild (string target, string guid) + { + PBXBuildFileData data = PBXBuildFileData.CreateFromFile (guid, false, null); + m_Data.BuildFilesAdd (target, data); + return data.guid; + } + + /// + /// Gets the guid of the resouce build phase of the specified target. + /// + /// The guid of the resource build phase. + /// Target name. + private string ResourceBuildPhaseByTargetName (string name) + { + PBXNativeTargetData target = TargetByName (name); + if (target == null) + return null; + // phases is a GUIDList containing the build phases + foreach (var phase in target.phases) { + // find the build phase in the list of resources + foreach (var resource in resources.GetEntries ()) { + if (resource.Value.guid == phase) + return resource.Value.guid; + } + } + return null; + } + + /// + /// Gets the native target by name. + /// + /// The target data. + /// Name. + private PBXNativeTargetData TargetByName (string name) + { + foreach (var entry in nativeTargets.GetEntries ()) { + if (entry.Value.name == name) + return entry.Value; + } + return null; + } + + /* + * Allows the setting of target attributes in the project section such as Provisioning Style and Team ID for each target + * + * The Target Attributes are structured like so: + * attributes = { + * TargetAttributes = { + * 1D6058900D05DD3D006BFB54 = { + * DevelopmentTeam = Z6SFPV59E3; + * ProvisioningStyle = Manual; + * }; + * 5623C57217FDCB0800090B9E = { + * DevelopmentTeam = Z6SFPV59E3; + * ProvisioningStyle = Manual; + * TestTargetID = 1D6058900D05DD3D006BFB54; + * }; + * }; + * }; + */ + internal void SetTargetAttributes(string key, string value) + { + PBXElementDict properties = project.project.GetPropertiesRaw(); + PBXElementDict attributes; + PBXElementDict targetAttributes; + if (properties.Contains("attributes")) + { + attributes = properties["attributes"] as PBXElementDict; + } + else + { + attributes = properties.CreateDict("attributes"); + } + + if (attributes.Contains("TargetAttributes")) + { + targetAttributes = attributes["TargetAttributes"] as PBXElementDict; + } + else + { + targetAttributes = attributes.CreateDict("TargetAttributes"); + } + + foreach (KeyValuePair target in nativeTargets.GetEntries()) { + PBXElementDict targetAttributesRaw; + if (targetAttributes.Contains(target.Key)) + { + targetAttributesRaw = targetAttributes[target.Key].AsDict(); + } + else + { + targetAttributesRaw = targetAttributes.CreateDict(target.Key); + } + targetAttributesRaw.SetString(key, value); + } + project.project.UpdateVars(); + + } + } +} // namespace UnityEditor.iOS.Xcode diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProject.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProject.cs.meta new file mode 100644 index 0000000..5d35d55 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProject.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 5923416dbf32e48428afbeee2b4eb48e +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectData.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectData.cs new file mode 100644 index 0000000..d0db1c2 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectData.cs @@ -0,0 +1,713 @@ +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; +using System; +using ChillyRoom.UnityEditor.iOS.Xcode.PBX; + +namespace ChillyRoom.UnityEditor.iOS.Xcode +{ + using PBXBuildFileSection = KnownSectionBase; + using PBXFileReferenceSection = KnownSectionBase; + using PBXGroupSection = KnownSectionBase; + using PBXContainerItemProxySection = KnownSectionBase; + using PBXReferenceProxySection = KnownSectionBase; + using PBXSourcesBuildPhaseSection = KnownSectionBase; + using PBXFrameworksBuildPhaseSection= KnownSectionBase; + using PBXResourcesBuildPhaseSection = KnownSectionBase; + using PBXCopyFilesBuildPhaseSection = KnownSectionBase; + using PBXShellScriptBuildPhaseSection = KnownSectionBase; + using PBXVariantGroupSection = KnownSectionBase; + using PBXNativeTargetSection = KnownSectionBase; + using PBXTargetDependencySection = KnownSectionBase; + using XCBuildConfigurationSection = KnownSectionBase; + using XCConfigurationListSection = KnownSectionBase; + using UnknownSection = KnownSectionBase; + + internal class PBXProjectData + { + private Dictionary m_Section = null; + private PBXElementDict m_RootElements = null; + private PBXElementDict m_UnknownObjects = null; + private string m_ObjectVersion = null; + private List m_SectionOrder = null; + + private Dictionary m_UnknownSections; + private PBXBuildFileSection buildFiles = null; // use BuildFiles* methods instead of manipulating directly + private PBXFileReferenceSection fileRefs = null; // use FileRefs* methods instead of manipulating directly + private PBXGroupSection groups = null; // use Groups* methods instead of manipulating directly + public PBXContainerItemProxySection containerItems = null; + public PBXReferenceProxySection references = null; + public PBXSourcesBuildPhaseSection sources = null; + public PBXFrameworksBuildPhaseSection frameworks = null; + public PBXResourcesBuildPhaseSection resources = null; + public PBXCopyFilesBuildPhaseSection copyFiles = null; + public PBXShellScriptBuildPhaseSection shellScripts = null; + public PBXNativeTargetSection nativeTargets = null; + public PBXTargetDependencySection targetDependencies = null; + public PBXVariantGroupSection variantGroups = null; + public XCBuildConfigurationSection buildConfigs = null; + public XCConfigurationListSection buildConfigLists = null; + public PBXProjectSection project = null; + + // FIXME: create a separate PBXObject tree to represent these relationships + + // A build file can be represented only once in all *BuildPhaseSection sections, thus + // we can simplify the cache by not caring about the file extension + private Dictionary> m_FileGuidToBuildFileMap = null; + private Dictionary m_ProjectPathToFileRefMap = null; + private Dictionary m_FileRefGuidToProjectPathMap = null; + private Dictionary> m_RealPathToFileRefMap = null; + private Dictionary m_ProjectPathToGroupMap = null; + private Dictionary m_GroupGuidToProjectPathMap = null; + private Dictionary m_GuidToParentGroupMap = null; + + public PBXBuildFileData BuildFilesGet(string guid) + { + return buildFiles[guid]; + } + + // targetGuid is the guid of the target that contains the section that contains the buildFile + public void BuildFilesAdd(string targetGuid, PBXBuildFileData buildFile) + { + if (!m_FileGuidToBuildFileMap.ContainsKey(targetGuid)) + m_FileGuidToBuildFileMap[targetGuid] = new Dictionary(); + m_FileGuidToBuildFileMap[targetGuid][buildFile.fileRef] = buildFile; + buildFiles.AddEntry(buildFile); + } + + public void BuildFilesRemove(string targetGuid, string fileGuid) + { + var buildFile = BuildFilesGetForSourceFile(targetGuid, fileGuid); + if (buildFile != null) + { + m_FileGuidToBuildFileMap[targetGuid].Remove(buildFile.fileRef); + buildFiles.RemoveEntry(buildFile.guid); + } + } + + public PBXBuildFileData BuildFilesGetForSourceFile(string targetGuid, string fileGuid) + { + if (!m_FileGuidToBuildFileMap.ContainsKey(targetGuid)) + return null; + if (!m_FileGuidToBuildFileMap[targetGuid].ContainsKey(fileGuid)) + return null; + return m_FileGuidToBuildFileMap[targetGuid][fileGuid]; + } + + public IEnumerable BuildFilesGetAll() + { + return buildFiles.GetObjects(); + } + + public void FileRefsAdd(string realPath, string projectPath, PBXGroupData parent, PBXFileReferenceData fileRef) + { + fileRefs.AddEntry(fileRef); + m_ProjectPathToFileRefMap.Add(projectPath, fileRef); + m_FileRefGuidToProjectPathMap.Add(fileRef.guid, projectPath); + //m_RealPathToFileRefMap[fileRef.tree].Add(realPath, fileRef); // FIXME + if (m_RealPathToFileRefMap.ContainsKey (fileRef.tree)) + m_RealPathToFileRefMap [fileRef.tree].Add (realPath, fileRef); // FIXME + m_GuidToParentGroupMap.Add(fileRef.guid, parent); + } + + public IEnumerable FileRefsGetAll () + { + return fileRefs.GetObjects(); + } + + public PBXFileReferenceData FileRefsGet(string guid) + { + return fileRefs[guid]; + } + + public PBXFileReferenceData FileRefsGetByRealPath(string path, PBXSourceTree sourceTree) + { + if (m_RealPathToFileRefMap[sourceTree].ContainsKey(path)) + return m_RealPathToFileRefMap[sourceTree][path]; + return null; + } + + public PBXFileReferenceData FileRefsGetByProjectPath(string path) + { + if (m_ProjectPathToFileRefMap.ContainsKey(path)) + return m_ProjectPathToFileRefMap[path]; + return null; + } + + public void FileRefsRemove(string guid) + { + PBXFileReferenceData fileRef = fileRefs[guid]; + fileRefs.RemoveEntry(guid); + m_ProjectPathToFileRefMap.Remove(m_FileRefGuidToProjectPathMap[guid]); + m_FileRefGuidToProjectPathMap.Remove(guid); + foreach (var tree in FileTypeUtils.AllAbsoluteSourceTrees()) + m_RealPathToFileRefMap[tree].Remove(fileRef.path); + m_GuidToParentGroupMap.Remove(guid); + } + + public PBXGroupData GroupsGet(string guid) + { + return groups[guid]; + } + + public PBXGroupData GroupsGetByChild(string childGuid) + { + return m_GuidToParentGroupMap[childGuid]; + } + + public PBXGroupData GroupsGetMainGroup() + { + return groups[project.project.mainGroup]; + } + + /// Returns the source group identified by sourceGroup. If sourceGroup is empty or null, + /// root group is returned. If no group is found, null is returned. + public PBXGroupData GroupsGetByProjectPath(string sourceGroup) + { + if (m_ProjectPathToGroupMap.ContainsKey(sourceGroup)) + return m_ProjectPathToGroupMap[sourceGroup]; + return null; + } + + public void GroupsAdd(string projectPath, PBXGroupData parent, PBXGroupData gr) + { + m_ProjectPathToGroupMap.Add(projectPath, gr); + m_GroupGuidToProjectPathMap.Add(gr.guid, projectPath); + m_GuidToParentGroupMap.Add(gr.guid, parent); + groups.AddEntry(gr); + } + + public void GroupsAddDuplicate(PBXGroupData gr) + { + groups.AddEntry(gr); + } + + public void GroupsRemove(string guid) + { + m_ProjectPathToGroupMap.Remove(m_GroupGuidToProjectPathMap[guid]); + m_GroupGuidToProjectPathMap.Remove(guid); + m_GuidToParentGroupMap.Remove(guid); + groups.RemoveEntry(guid); + } + + public FileGUIDListBase BuildSectionAny(PBXNativeTargetData target, string path, bool isFolderRef) + { + string ext = Path.GetExtension(path); + var phase = FileTypeUtils.GetFileType(ext, isFolderRef); + switch (phase) { + case PBXFileType.Framework: + foreach (var guid in target.phases) + if (frameworks.HasEntry(guid)) + return frameworks[guid]; + break; + case PBXFileType.Resource: + foreach (var guid in target.phases) + if (resources.HasEntry(guid)) + return resources[guid]; + break; + case PBXFileType.Source: + foreach (var guid in target.phases) + if (sources.HasEntry(guid)) + return sources[guid]; + break; + case PBXFileType.CopyFile: + foreach (var guid in target.phases) + if (copyFiles.HasEntry(guid)) + return copyFiles[guid]; + break; + } + return null; + } + + public FileGUIDListBase BuildSectionAny(string sectionGuid) + { + if (frameworks.HasEntry(sectionGuid)) + return frameworks[sectionGuid]; + if (resources.HasEntry(sectionGuid)) + return resources[sectionGuid]; + if (sources.HasEntry(sectionGuid)) + return sources[sectionGuid]; + if (copyFiles.HasEntry(sectionGuid)) + return copyFiles[sectionGuid]; + throw new Exception(String.Format("The given GUID {0} does not refer to a known build section", sectionGuid)); + } + + void RefreshBuildFilesMapForBuildFileGuidList(Dictionary mapForTarget, + FileGUIDListBase list) + { + foreach (string guid in list.files) + { + var buildFile = buildFiles[guid]; + mapForTarget[buildFile.fileRef] = buildFile; + } + } + + void RefreshMapsForGroupChildren(string projectPath, string realPath, PBXSourceTree realPathTree, PBXGroupData parent) + { + var children = new List(parent.children); + foreach (string guid in children) + { + PBXFileReferenceData fileRef = fileRefs[guid]; + string pPath; + string rPath; + PBXSourceTree rTree; + + if (fileRef != null) + { + pPath = PBXPath.Combine(projectPath, fileRef.name); + PBXPath.Combine(realPath, realPathTree, fileRef.path, fileRef.tree, out rPath, out rTree); + + if (!m_ProjectPathToFileRefMap.ContainsKey(pPath)) + { + m_ProjectPathToFileRefMap.Add(pPath, fileRef); + } + if (!m_FileRefGuidToProjectPathMap.ContainsKey(fileRef.guid)) + { + m_FileRefGuidToProjectPathMap.Add(fileRef.guid, pPath); + } + if (!m_RealPathToFileRefMap[rTree].ContainsKey(rPath)) + { + m_RealPathToFileRefMap[rTree].Add(rPath, fileRef); + } + if (!m_GuidToParentGroupMap.ContainsKey(guid)) + { + m_GuidToParentGroupMap.Add(guid, parent); + } + + continue; + } + + PBXGroupData gr = groups[guid]; + if (gr != null) + { + pPath = PBXPath.Combine(projectPath, gr.name); + PBXPath.Combine(realPath, realPathTree, gr.path, gr.tree, out rPath, out rTree); + + if (!m_ProjectPathToGroupMap.ContainsKey(pPath)) + { + m_ProjectPathToGroupMap.Add(pPath, gr); + } + if (!m_GroupGuidToProjectPathMap.ContainsKey(gr.guid)) + { + m_GroupGuidToProjectPathMap.Add(gr.guid, pPath); + } + if (!m_GuidToParentGroupMap.ContainsKey(guid)) + { + m_GuidToParentGroupMap.Add(guid, parent); + } + + RefreshMapsForGroupChildren(pPath, rPath, rTree, gr); + } + } + } + + void RefreshAuxMaps() + { + foreach (var targetEntry in nativeTargets.GetEntries()) + { + var map = new Dictionary(); + foreach (string phaseGuid in targetEntry.Value.phases) + { + if (frameworks.HasEntry(phaseGuid)) + RefreshBuildFilesMapForBuildFileGuidList(map, frameworks[phaseGuid]); + if (resources.HasEntry(phaseGuid)) + RefreshBuildFilesMapForBuildFileGuidList(map, resources[phaseGuid]); + if (sources.HasEntry(phaseGuid)) + RefreshBuildFilesMapForBuildFileGuidList(map, sources[phaseGuid]); + if (copyFiles.HasEntry(phaseGuid)) + RefreshBuildFilesMapForBuildFileGuidList(map, copyFiles[phaseGuid]); + } + m_FileGuidToBuildFileMap[targetEntry.Key] = map; + } + RefreshMapsForGroupChildren("", "", PBXSourceTree.Source, GroupsGetMainGroup()); + } + + public void Clear() + { + buildFiles = new PBXBuildFileSection("PBXBuildFile"); + fileRefs = new PBXFileReferenceSection("PBXFileReference"); + groups = new PBXGroupSection("PBXGroup"); + containerItems = new PBXContainerItemProxySection("PBXContainerItemProxy"); + references = new PBXReferenceProxySection("PBXReferenceProxy"); + sources = new PBXSourcesBuildPhaseSection("PBXSourcesBuildPhase"); + frameworks = new PBXFrameworksBuildPhaseSection("PBXFrameworksBuildPhase"); + resources = new PBXResourcesBuildPhaseSection("PBXResourcesBuildPhase"); + copyFiles = new PBXCopyFilesBuildPhaseSection("PBXCopyFilesBuildPhase"); + shellScripts = new PBXShellScriptBuildPhaseSection("PBXShellScriptBuildPhase"); + nativeTargets = new PBXNativeTargetSection("PBXNativeTarget"); + targetDependencies = new PBXTargetDependencySection("PBXTargetDependency"); + variantGroups = new PBXVariantGroupSection("PBXVariantGroup"); + buildConfigs = new XCBuildConfigurationSection("XCBuildConfiguration"); + buildConfigLists = new XCConfigurationListSection("XCConfigurationList"); + project = new PBXProjectSection(); + m_UnknownSections = new Dictionary(); + + m_Section = new Dictionary + { + { "PBXBuildFile", buildFiles }, + { "PBXFileReference", fileRefs }, + { "PBXGroup", groups }, + { "PBXContainerItemProxy", containerItems }, + { "PBXReferenceProxy", references }, + { "PBXSourcesBuildPhase", sources }, + { "PBXFrameworksBuildPhase", frameworks }, + { "PBXResourcesBuildPhase", resources }, + { "PBXCopyFilesBuildPhase", copyFiles }, + { "PBXShellScriptBuildPhase", shellScripts }, + { "PBXNativeTarget", nativeTargets }, + { "PBXTargetDependency", targetDependencies }, + { "PBXVariantGroup", variantGroups }, + { "XCBuildConfiguration", buildConfigs }, + { "XCConfigurationList", buildConfigLists }, + + { "PBXProject", project }, + }; + m_RootElements = new PBXElementDict(); + m_UnknownObjects = new PBXElementDict(); + m_ObjectVersion = null; + m_SectionOrder = new List{ + "PBXBuildFile", "PBXContainerItemProxy", "PBXCopyFilesBuildPhase", "PBXFileReference", + "PBXFrameworksBuildPhase", "PBXGroup", "PBXNativeTarget", "PBXProject", "PBXReferenceProxy", + "PBXResourcesBuildPhase", "PBXShellScriptBuildPhase", "PBXSourcesBuildPhase", "PBXTargetDependency", + "PBXVariantGroup", "XCBuildConfiguration", "XCConfigurationList" + }; + m_FileGuidToBuildFileMap = new Dictionary>(); + m_ProjectPathToFileRefMap = new Dictionary(); + m_FileRefGuidToProjectPathMap = new Dictionary(); + m_RealPathToFileRefMap = new Dictionary>(); + foreach (var tree in FileTypeUtils.AllAbsoluteSourceTrees()) + m_RealPathToFileRefMap.Add(tree, new Dictionary()); + m_ProjectPathToGroupMap = new Dictionary(); + m_GroupGuidToProjectPathMap = new Dictionary(); + m_GuidToParentGroupMap = new Dictionary(); + } + + private void BuildCommentMapForBuildFiles(GUIDToCommentMap comments, List guids, string sectName) + { + foreach (var guid in guids) + { + var buildFile = BuildFilesGet(guid); + if (buildFile != null) + { + var fileRef = FileRefsGet(buildFile.fileRef); + if (fileRef != null) + comments.Add(guid, String.Format("{0} in {1}", fileRef.name, sectName)); + else + { + var reference = references[buildFile.fileRef]; + if (reference != null) + comments.Add(guid, String.Format("{0} in {1}", reference.path, sectName)); + } + } + } + } + + private GUIDToCommentMap BuildCommentMap() + { + GUIDToCommentMap comments = new GUIDToCommentMap(); + + // buildFiles are handled below + // filerefs are handled below + foreach (var e in groups.GetObjects()) + comments.Add(e.guid, e.name); + foreach (var e in containerItems.GetObjects()) + comments.Add(e.guid, "PBXContainerItemProxy"); + foreach (var e in references.GetObjects()) + comments.Add(e.guid, e.path); + foreach (var e in sources.GetObjects()) + { + comments.Add(e.guid, "Sources"); + BuildCommentMapForBuildFiles(comments, e.files, "Sources"); + } + foreach (var e in resources.GetObjects()) + { + comments.Add(e.guid, "Resources"); + BuildCommentMapForBuildFiles(comments, e.files, "Resources"); + } + foreach (var e in frameworks.GetObjects()) + { + comments.Add(e.guid, "Frameworks"); + BuildCommentMapForBuildFiles(comments, e.files, "Frameworks"); + } + foreach (var e in copyFiles.GetObjects()) + { + string sectName = e.name; + if (sectName == null) + sectName = "CopyFiles"; + comments.Add(e.guid, sectName); + BuildCommentMapForBuildFiles(comments, e.files, sectName); + } + foreach (var e in shellScripts.GetObjects()) + comments.Add(e.guid, "ShellScript"); + foreach (var e in targetDependencies.GetObjects()) + comments.Add(e.guid, "PBXTargetDependency"); + foreach (var e in nativeTargets.GetObjects()) + { + comments.Add(e.guid, e.name); + comments.Add(e.buildConfigList, String.Format("Build configuration list for PBXNativeTarget \"{0}\"", e.name)); + } + foreach (var e in variantGroups.GetObjects()) + comments.Add(e.guid, e.name); + foreach (var e in buildConfigs.GetObjects()) + comments.Add(e.guid, e.name); + foreach (var e in project.GetObjects()) + { + comments.Add(e.guid, "Project object"); + comments.Add(e.buildConfigList, "Build configuration list for PBXProject \"Unity-iPhone\""); // FIXME: project name is hardcoded + } + foreach (var e in fileRefs.GetObjects()) + comments.Add(e.guid, e.name); + if (m_RootElements.Contains("rootObject") && m_RootElements["rootObject"] is PBXElementString) + comments.Add(m_RootElements["rootObject"].AsString(), "Project object"); + + return comments; + } + + private static PBXElementDict ParseContent(string content) + { + TokenList tokens = Lexer.Tokenize(content); + var parser = new Parser(tokens); + TreeAST ast = parser.ParseTree(); + return Serializer.ParseTreeAST(ast, tokens, content); + } + + public void ReadFromStream(TextReader sr) + { + Clear(); + m_RootElements = ParseContent(sr.ReadToEnd()); + + if (!m_RootElements.Contains("objects")) + throw new Exception("Invalid PBX project file: no objects element"); + + var objects = m_RootElements["objects"].AsDict(); + m_RootElements.Remove("objects"); + m_RootElements.SetString("objects", "OBJMARKER"); + + if (m_RootElements.Contains("objectVersion")) + { + m_ObjectVersion = m_RootElements["objectVersion"].AsString(); + m_RootElements.Remove("objectVersion"); + } + + var allGuids = new List(); + string prevSectionName = null; + foreach (var kv in objects.values) + { + allGuids.Add(kv.Key); + var el = kv.Value; + + if (!(el is PBXElementDict) || !el.AsDict().Contains("isa")) + { + m_UnknownObjects.values.Add(kv.Key, el); + continue; + } + var dict = el.AsDict(); + var sectionName = dict["isa"].AsString(); + + if (m_Section.ContainsKey(sectionName)) + { + var section = m_Section[sectionName]; + section.AddObject(kv.Key, dict); + } + else + { + UnknownSection section; + if (m_UnknownSections.ContainsKey(sectionName)) + section = m_UnknownSections[sectionName]; + else + { + section = new UnknownSection(sectionName); + m_UnknownSections.Add(sectionName, section); + } + section.AddObject(kv.Key, dict); + + // update section order + if (!m_SectionOrder.Contains(sectionName)) + { + int pos = 0; + if (prevSectionName != null) + { + // this never fails, because we already added any previous unknown sections + // to m_SectionOrder + pos = m_SectionOrder.FindIndex(x => x == prevSectionName); + pos += 1; + } + m_SectionOrder.Insert(pos, sectionName); + } + } + prevSectionName = sectionName; + } + RepairStructure(allGuids); + RefreshAuxMaps(); + } + + + public string WriteToString() + { + var commentMap = BuildCommentMap(); + var emptyChecker = new PropertyCommentChecker(); + var emptyCommentMap = new GUIDToCommentMap(); + + // since we need to add custom comments, the serialization is much more complex + StringBuilder objectsSb = new StringBuilder(); + if (m_ObjectVersion != null) // objectVersion comes right before objects + objectsSb.AppendFormat("objectVersion = {0};\n\t", m_ObjectVersion); + objectsSb.Append("objects = {"); + foreach (string sectionName in m_SectionOrder) + { + if (m_Section.ContainsKey(sectionName)) + m_Section[sectionName].WriteSection(objectsSb, commentMap); + else if (m_UnknownSections.ContainsKey(sectionName)) + m_UnknownSections[sectionName].WriteSection(objectsSb, commentMap); + } + foreach (var kv in m_UnknownObjects.values) + Serializer.WriteDictKeyValue(objectsSb, kv.Key, kv.Value, 2, false, emptyChecker, emptyCommentMap); + objectsSb.Append("\n\t};"); + + StringBuilder contentSb = new StringBuilder(); + contentSb.Append("// !$*UTF8*$!\n"); + Serializer.WriteDict(contentSb, m_RootElements, 0, false, + new PropertyCommentChecker(new string[]{"rootObject/*"}), commentMap); + contentSb.Append("\n"); + string content = contentSb.ToString(); + + content = content.Replace("objects = OBJMARKER;", objectsSb.ToString()); + return content; + } + + // This method walks the project structure and removes invalid entries. + void RepairStructure(List allGuids) + { + var guidSet = new Dictionary(); // emulate HashSet on .Net 2.0 + foreach (var guid in allGuids) + guidSet.Add(guid, false); + + while (RepairStructureImpl(guidSet) == true) + ; + } + + /* Iterates the given guid list and removes all guids that are not in allGuids dictionary. + */ + static void RemoveMissingGuidsFromGuidList(PBX.GUIDList guidList, Dictionary allGuids) + { + List guidsToRemove = null; + foreach (var guid in guidList) + { + if (!allGuids.ContainsKey(guid)) + { + if (guidsToRemove == null) + guidsToRemove = new List(); + guidsToRemove.Add(guid); + } + } + if (guidsToRemove != null) + { + foreach (var guid in guidsToRemove) + guidList.RemoveGUID(guid); + } + } + + /* Removes objects from the given @a section for which @a checker returns true. + Also removes the guids of the removed elements from allGuids dictionary. + Returns true if any objects were removed. + */ + static bool RemoveObjectsFromSection(KnownSectionBase section, + Dictionary allGuids, + Func checker) where T : PBXObjectData, new() + { + List guidsToRemove = null; + foreach (var kv in section.GetEntries()) + { + if (checker(kv.Value)) + { + if (guidsToRemove == null) + guidsToRemove = new List(); + guidsToRemove.Add(kv.Key); + } + } + if (guidsToRemove != null) + { + foreach (var guid in guidsToRemove) + { + section.RemoveEntry(guid); + allGuids.Remove(guid); + } + return true; + } + return false; + } + + // Returns true if changes were done and one should call RepairStructureImpl again + bool RepairStructureImpl(Dictionary allGuids) + { + bool changed = false; + + // PBXBuildFile + changed |= RemoveObjectsFromSection(buildFiles, allGuids, + o => (o.fileRef == null || !allGuids.ContainsKey(o.fileRef))); + // PBXFileReference / fileRefs not cleaned + + // PBXGroup + changed |= RemoveObjectsFromSection(groups, allGuids, o => o.children == null); + foreach (var o in groups.GetObjects()) + RemoveMissingGuidsFromGuidList(o.children, allGuids); + + // PBXContainerItem / containerItems not cleaned + // PBXReferenceProxy / references not cleaned + + // PBXSourcesBuildPhase + changed |= RemoveObjectsFromSection(sources, allGuids, o => o.files == null); + foreach (var o in sources.GetObjects()) + RemoveMissingGuidsFromGuidList(o.files, allGuids); + // PBXFrameworksBuildPhase + changed |= RemoveObjectsFromSection(frameworks, allGuids, o => o.files == null); + foreach (var o in frameworks.GetObjects()) + RemoveMissingGuidsFromGuidList(o.files, allGuids); + // PBXResourcesBuildPhase + changed |= RemoveObjectsFromSection(resources, allGuids, o => o.files == null); + foreach (var o in resources.GetObjects()) + RemoveMissingGuidsFromGuidList(o.files, allGuids); + // PBXCopyFilesBuildPhase + changed |= RemoveObjectsFromSection(copyFiles, allGuids, o => o.files == null); + foreach (var o in copyFiles.GetObjects()) + RemoveMissingGuidsFromGuidList(o.files, allGuids); + // PBXShellScriptsBuildPhase + changed |= RemoveObjectsFromSection(shellScripts, allGuids, o => o.files == null); + foreach (var o in shellScripts.GetObjects()) + RemoveMissingGuidsFromGuidList(o.files, allGuids); + + // PBXNativeTarget + changed |= RemoveObjectsFromSection(nativeTargets, allGuids, o => o.phases == null); + foreach (var o in nativeTargets.GetObjects()) + RemoveMissingGuidsFromGuidList(o.phases, allGuids); + + // PBXTargetDependency / targetDependencies not cleaned + + // PBXVariantGroup + changed |= RemoveObjectsFromSection(variantGroups, allGuids, o => o.children == null); + foreach (var o in variantGroups.GetObjects()) + RemoveMissingGuidsFromGuidList(o.children, allGuids); + + // XCBuildConfiguration / buildConfigs not cleaned + + // XCConfigurationList + changed |= RemoveObjectsFromSection(buildConfigLists, allGuids, o => o.buildConfigs == null); + foreach (var o in buildConfigLists.GetObjects()) + RemoveMissingGuidsFromGuidList(o.buildConfigs, allGuids); + + // PBXProject project not cleaned + return changed; + } + + public PBXGroupData GroupsGetByName (string name) + { + foreach (var group in groups.GetEntries()) + if (group.Value.name == name) + return group.Value; + return null; + } + } + +} // namespace UnityEditor.iOS.Xcode + diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectData.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectData.cs.meta new file mode 100644 index 0000000..182f43a --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectData.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 54c86bc46b87645c78e5b211a0eeada5 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectExtensions.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectExtensions.cs new file mode 100644 index 0000000..e6497a3 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectExtensions.cs @@ -0,0 +1,296 @@ +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; +using System; +using ChillyRoom.UnityEditor.iOS.Xcode.PBX; + +namespace ChillyRoom.UnityEditor.iOS.Xcode.Extensions +{ + /* This class implements a number of static methods for performing common tasks + on xcode projects. + TODO: Make sure enough stuff is exposed so that it's possible to perform the tasks + without using internal APIs + */ + public static class PBXProjectExtensions + { + // Create a wrapper class so that collection initializers work and we can have a + // compact notation. Note that we can't use Dictionary because the keys may be duplicate + internal class FlagList : List> + { + public void Add(string flag, string value) + { + Add(new KeyValuePair(flag, value)); + } + } + + internal static FlagList appExtensionReleaseBuildFlags = new FlagList + { + // { "INFOPLIST_FILE", }, + { "LD_RUNPATH_SEARCH_PATHS", "$(inherited)" }, + { "LD_RUNPATH_SEARCH_PATHS", "@executable_path/Frameworks" }, + { "LD_RUNPATH_SEARCH_PATHS", "@executable_path/../../Frameworks" }, + // { "PRODUCT_BUNDLE_IDENTIFIER", "" }, + { "PRODUCT_NAME", "$(TARGET_NAME)" }, + { "SKIP_INSTALL", "YES" }, + }; + + internal static FlagList appExtensionDebugBuildFlags = new FlagList + { + // { "INFOPLIST_FILE", }, + { "LD_RUNPATH_SEARCH_PATHS", "$(inherited)" }, + { "LD_RUNPATH_SEARCH_PATHS", "@executable_path/Frameworks" }, + { "LD_RUNPATH_SEARCH_PATHS", "@executable_path/../../Frameworks" }, + // { "PRODUCT_BUNDLE_IDENTIFIER", "" }, + { "PRODUCT_NAME", "$(TARGET_NAME)" }, + { "SKIP_INSTALL", "YES" }, + }; + + internal static FlagList watchExtensionReleaseBuildFlags = new FlagList + { + { "ASSETCATALOG_COMPILER_COMPLICATION_NAME", "Complication" }, + { "CLANG_ANALYZER_NONNULL", "YES" }, + { "CLANG_WARN_DOCUMENTATION_COMMENTS", "YES" }, + { "CLANG_WARN_INFINITE_RECURSION", "YES" }, + { "CLANG_WARN_SUSPICIOUS_MOVE", "YES" }, + { "DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym" }, + { "GCC_NO_COMMON_BLOCKS", "YES" }, + //{ "INFOPLIST_FILE", "" }, + { "LD_RUNPATH_SEARCH_PATHS", "$(inherited)" }, + { "LD_RUNPATH_SEARCH_PATHS", "@executable_path/Frameworks" }, + { "LD_RUNPATH_SEARCH_PATHS", "@executable_path/../../Frameworks" }, + // { "PRODUCT_BUNDLE_IDENTIFIER", "" }, + { "PRODUCT_NAME", "${TARGET_NAME}" }, + { "SDKROOT", "watchos" }, + { "SKIP_INSTALL", "YES" }, + { "TARGETED_DEVICE_FAMILY", "4" }, + { "WATCHOS_DEPLOYMENT_TARGET", "3.1" }, + // the following are needed to override project settings in Unity Xcode project + { "ARCHS", "$(ARCHS_STANDARD)" }, + { "SUPPORTED_PLATFORMS", "watchos" }, + { "SUPPORTED_PLATFORMS", "watchsimulator" }, + }; + + internal static FlagList watchExtensionDebugBuildFlags = new FlagList + { + { "ASSETCATALOG_COMPILER_COMPLICATION_NAME", "Complication" }, + { "CLANG_ANALYZER_NONNULL", "YES" }, + { "CLANG_WARN_DOCUMENTATION_COMMENTS", "YES" }, + { "CLANG_WARN_INFINITE_RECURSION", "YES" }, + { "CLANG_WARN_SUSPICIOUS_MOVE", "YES" }, + { "DEBUG_INFORMATION_FORMAT", "dwarf" }, + { "ENABLE_TESTABILITY", "YES" }, + { "GCC_NO_COMMON_BLOCKS", "YES" }, + // { "INFOPLIST_FILE", "" }, + { "LD_RUNPATH_SEARCH_PATHS", "$(inherited)" }, + { "LD_RUNPATH_SEARCH_PATHS", "@executable_path/Frameworks" }, + { "LD_RUNPATH_SEARCH_PATHS", "@executable_path/../../Frameworks" }, + // { "PRODUCT_BUNDLE_IDENTIFIER", "" }, + { "PRODUCT_NAME", "${TARGET_NAME}" }, + { "SDKROOT", "watchos" }, + { "SKIP_INSTALL", "YES" }, + { "TARGETED_DEVICE_FAMILY", "4" }, + { "WATCHOS_DEPLOYMENT_TARGET", "3.1" }, + // the following are needed to override project settings in Unity Xcode project + { "ARCHS", "$(ARCHS_STANDARD)" }, + { "SUPPORTED_PLATFORMS", "watchos" }, + { "SUPPORTED_PLATFORMS", "watchsimulator" }, + }; + + internal static FlagList watchAppReleaseBuildFlags = new FlagList + { + { "ASSETCATALOG_COMPILER_APPICON_NAME", "AppIcon" }, + { "CLANG_ANALYZER_NONNULL", "YES" }, + { "CLANG_WARN_DOCUMENTATION_COMMENTS", "YES" }, + { "CLANG_WARN_INFINITE_RECURSION", "YES" }, + { "CLANG_WARN_SUSPICIOUS_MOVE", "YES" }, + { "DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym" }, + { "GCC_NO_COMMON_BLOCKS", "YES" }, + //{ "IBSC_MODULE", "the extension target name with ' ' replaced with '_'" }, + //{ "INFOPLIST_FILE", "" }, + //{ "PRODUCT_BUNDLE_IDENTIFIER", "" }, + { "PRODUCT_NAME", "$(TARGET_NAME)" }, + { "SDKROOT", "watchos" }, + { "SKIP_INSTALL", "YES" }, + { "TARGETED_DEVICE_FAMILY", "4" }, + { "WATCHOS_DEPLOYMENT_TARGET", "3.1" }, + // the following are needed to override project settings in Unity Xcode project + { "ARCHS", "$(ARCHS_STANDARD)" }, + { "SUPPORTED_PLATFORMS", "watchos" }, + { "SUPPORTED_PLATFORMS", "watchsimulator" }, + }; + + internal static FlagList watchAppDebugBuildFlags = new FlagList + { + { "ASSETCATALOG_COMPILER_APPICON_NAME", "AppIcon" }, + { "CLANG_ANALYZER_NONNULL", "YES" }, + { "CLANG_WARN_DOCUMENTATION_COMMENTS", "YES" }, + { "CLANG_WARN_INFINITE_RECURSION", "YES" }, + { "CLANG_WARN_SUSPICIOUS_MOVE", "YES" }, + { "DEBUG_INFORMATION_FORMAT", "dwarf" }, + { "ENABLE_TESTABILITY", "YES" }, + { "GCC_NO_COMMON_BLOCKS", "YES" }, + //{ "IBSC_MODULE", "the extension target name with ' ' replaced with '_'" }, + //{ "INFOPLIST_FILE", "" }, + //{ "PRODUCT_BUNDLE_IDENTIFIER", "" }, + { "PRODUCT_NAME", "$(TARGET_NAME)" }, + { "SDKROOT", "watchos" }, + { "SKIP_INSTALL", "YES" }, + { "TARGETED_DEVICE_FAMILY", "4" }, + { "WATCHOS_DEPLOYMENT_TARGET", "3.1" }, + // the following are needed to override project settings in Unity Xcode project + { "ARCHS", "$(ARCHS_STANDARD)" }, + { "SUPPORTED_PLATFORMS", "watchos" }, + { "SUPPORTED_PLATFORMS", "watchsimulator" }, + }; + + static void SetBuildFlagsFromDict(this PBXProject proj, string configGuid, IEnumerable> data) + { + foreach (var kv in data) + proj.AddBuildPropertyForConfig(configGuid, kv.Key, kv.Value); + } + + internal static void SetDefaultAppExtensionReleaseBuildFlags(this PBXProject proj, string configGuid) + { + SetBuildFlagsFromDict(proj, configGuid, appExtensionReleaseBuildFlags); + } + + internal static void SetDefaultAppExtensionDebugBuildFlags(this PBXProject proj, string configGuid) + { + SetBuildFlagsFromDict(proj, configGuid, appExtensionDebugBuildFlags); + } + + internal static void SetDefaultWatchExtensionReleaseBuildFlags(this PBXProject proj, string configGuid) + { + SetBuildFlagsFromDict(proj, configGuid, watchExtensionReleaseBuildFlags); + } + + internal static void SetDefaultWatchExtensionDebugBuildFlags(this PBXProject proj, string configGuid) + { + SetBuildFlagsFromDict(proj, configGuid, watchExtensionDebugBuildFlags); + } + + internal static void SetDefaultWatchAppReleaseBuildFlags(this PBXProject proj, string configGuid) + { + SetBuildFlagsFromDict(proj, configGuid, watchAppReleaseBuildFlags); + } + + internal static void SetDefaultWatchAppDebugBuildFlags(this PBXProject proj, string configGuid) + { + SetBuildFlagsFromDict(proj, configGuid, watchAppDebugBuildFlags); + } + + /// + /// Creates an app extension. + /// + /// The GUID of the new target. + /// A project passed as this argument. + /// The GUID of the main target to link the app to. + /// The name of the app extension. + /// The bundle ID of the app extension. The bundle ID must be + /// prefixed with the parent app bundle ID. + /// Path to the app extension Info.plist document. + public static string AddAppExtension(this PBXProject proj, string mainTargetGuid, + string name, string bundleId, string infoPlistPath) + { + string ext = ".appex"; + var newTargetGuid = proj.AddTarget(name, ext, "com.apple.product-type.app-extension"); + + foreach (var configName in proj.BuildConfigNames()) + { + var configGuid = proj.BuildConfigByName(newTargetGuid, configName); + if (configName.Contains("Debug")) + SetDefaultAppExtensionDebugBuildFlags(proj, configGuid); + else + SetDefaultAppExtensionReleaseBuildFlags(proj, configGuid); + proj.SetBuildPropertyForConfig(configGuid, "INFOPLIST_FILE", infoPlistPath); + proj.SetBuildPropertyForConfig(configGuid, "PRODUCT_BUNDLE_IDENTIFIER", bundleId); + } + + proj.AddSourcesBuildPhase(newTargetGuid); + proj.AddResourcesBuildPhase(newTargetGuid); + proj.AddFrameworksBuildPhase(newTargetGuid); + string copyFilesPhaseGuid = proj.AddCopyFilesBuildPhase(mainTargetGuid, "Embed App Extensions", "", "13"); + proj.AddFileToBuildSection(mainTargetGuid, copyFilesPhaseGuid, proj.GetTargetProductFileRef(newTargetGuid)); + + proj.AddTargetDependency(mainTargetGuid, newTargetGuid); + + return newTargetGuid; + } + + /// + /// Creates a watch application. + /// + /// The GUID of the new target. + /// A project passed as this argument. + /// The GUID of the main target to link the watch app to. + /// The GUID of watch extension as returned by [[AddWatchExtension()]]. + /// The name of the watch app. It must the same as the name of the watch extension. + /// The bundle ID of the watch app. + /// Path to the watch app Info.plist document. + public static string AddWatchApp(this PBXProject proj, string mainTargetGuid, string watchExtensionTargetGuid, + string name, string bundleId, string infoPlistPath) + { + var newTargetGuid = proj.AddTarget(name, ".app", "com.apple.product-type.application.watchapp2"); + + var isbcModuleName = proj.nativeTargets[watchExtensionTargetGuid].name.Replace(" ", "_"); + + foreach (var configName in proj.BuildConfigNames()) + { + var configGuid = proj.BuildConfigByName(newTargetGuid, configName); + if (configName.Contains("Debug")) + SetDefaultWatchAppDebugBuildFlags(proj, configGuid); + else + SetDefaultWatchAppReleaseBuildFlags(proj, configGuid); + proj.SetBuildPropertyForConfig(configGuid, "PRODUCT_BUNDLE_IDENTIFIER", bundleId); + proj.SetBuildPropertyForConfig(configGuid, "INFOPLIST_FILE", infoPlistPath); + proj.SetBuildPropertyForConfig(configGuid, "IBSC_MODULE", isbcModuleName); + } + + proj.AddResourcesBuildPhase(newTargetGuid); + string copyFilesGuid = proj.AddCopyFilesBuildPhase(newTargetGuid, "Embed App Extensions", "", "13"); + proj.AddFileToBuildSection(newTargetGuid, copyFilesGuid, proj.GetTargetProductFileRef(watchExtensionTargetGuid)); + + string copyWatchFilesGuid = proj.AddCopyFilesBuildPhase(mainTargetGuid, "Embed Watch Content", "$(CONTENTS_FOLDER_PATH)/Watch", "16"); + proj.AddFileToBuildSection(mainTargetGuid, copyWatchFilesGuid, proj.GetTargetProductFileRef(newTargetGuid)); + + proj.AddTargetDependency(newTargetGuid, watchExtensionTargetGuid); + proj.AddTargetDependency(mainTargetGuid, newTargetGuid); + + return newTargetGuid; + } + + /// + /// Creates a watch extension. + /// + /// The GUID of the new target. + /// A project passed as this argument. + /// The GUID of the main target to link the watch extension to. + /// The name of the watch extension. + /// The bundle ID of the watch extension. The bundle ID must be + /// prefixed with the parent watch app bundle ID. + /// Path to the watch extension Info.plist document. + public static string AddWatchExtension(this PBXProject proj, string mainTarget, + string name, string bundleId, string infoPlistPath) + { + var newTargetGuid = proj.AddTarget(name, ".appex", "com.apple.product-type.watchkit2-extension"); + + foreach (var configName in proj.BuildConfigNames()) + { + var configGuid = proj.BuildConfigByName(newTargetGuid, configName); + if (configName.Contains("Debug")) + SetDefaultWatchExtensionDebugBuildFlags(proj, configGuid); + else + SetDefaultWatchExtensionReleaseBuildFlags(proj, configGuid); + proj.SetBuildPropertyForConfig(configGuid, "PRODUCT_BUNDLE_IDENTIFIER", bundleId); + proj.SetBuildPropertyForConfig(configGuid, "INFOPLIST_FILE", infoPlistPath); + } + + proj.AddSourcesBuildPhase(newTargetGuid); + proj.AddResourcesBuildPhase(newTargetGuid); + proj.AddFrameworksBuildPhase(newTargetGuid); + + return newTargetGuid; + } + } +} // namespace UnityEditor.iOS.Xcode diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectExtensions.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectExtensions.cs.meta new file mode 100644 index 0000000..6309b07 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PBXProjectExtensions.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: b84f17869f54f4d7a952c9f63e5744d6 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PlistParser.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/PlistParser.cs new file mode 100644 index 0000000..0f325ab --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PlistParser.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace ChillyRoom.UnityEditor.iOS.Xcode +{ + + public class PlistElement + { + protected PlistElement() {} + + // convenience methods + public string AsString() { return ((PlistElementString)this).value; } + public int AsInteger() { return ((PlistElementInteger)this).value; } + public bool AsBoolean() { return ((PlistElementBoolean)this).value; } + public PlistElementArray AsArray() { return (PlistElementArray)this; } + public PlistElementDict AsDict() { return (PlistElementDict)this; } + + public PlistElement this[string key] + { + get { return AsDict()[key]; } + set { AsDict()[key] = value; } + } + } + + public class PlistElementString : PlistElement + { + public PlistElementString(string v) { value = v; } + + public string value; + } + + public class PlistElementInteger : PlistElement + { + public PlistElementInteger(int v) { value = v; } + + public int value; + } + + public class PlistElementBoolean : PlistElement + { + public PlistElementBoolean(bool v) { value = v; } + + public bool value; + } + + public class PlistElementDict : PlistElement + { + public PlistElementDict() : base() {} + + private SortedDictionary m_PrivateValue = new SortedDictionary(); + public IDictionary values { get { return m_PrivateValue; }} + + new public PlistElement this[string key] + { + get { + if (values.ContainsKey(key)) + return values[key]; + return null; + } + set { this.values[key] = value; } + } + + + // convenience methods + public void SetInteger(string key, int val) + { + values[key] = new PlistElementInteger(val); + } + + public void SetString(string key, string val) + { + values[key] = new PlistElementString(val); + } + + public void SetBoolean(string key, bool val) + { + values[key] = new PlistElementBoolean(val); + } + + public PlistElementArray CreateArray(string key) + { + var v = new PlistElementArray(); + values[key] = v; + return v; + } + + public PlistElementDict CreateDict(string key) + { + var v = new PlistElementDict(); + values[key] = v; + return v; + } + } + + public class PlistElementArray : PlistElement + { + public PlistElementArray() : base() {} + public List values = new List(); + + // convenience methods + public void AddString(string val) + { + values.Add(new PlistElementString(val)); + } + + public void AddInteger(int val) + { + values.Add(new PlistElementInteger(val)); + } + + public void AddBoolean(bool val) + { + values.Add(new PlistElementBoolean(val)); + } + + public PlistElementArray AddArray() + { + var v = new PlistElementArray(); + values.Add(v); + return v; + } + + public PlistElementDict AddDict() + { + var v = new PlistElementDict(); + values.Add(v); + return v; + } + } + + public class PlistDocument + { + public PlistElementDict root; + public string version; + + public PlistDocument() + { + root = new PlistElementDict(); + version = "1.0"; + } + + // Parses a string that contains a XML file. No validation is done. + internal static XDocument ParseXmlNoDtd(string text) + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.ProhibitDtd = false; + settings.XmlResolver = null; // prevent DTD download + + XmlReader xmlReader = XmlReader.Create(new StringReader(text), settings); + return XDocument.Load(xmlReader); + } + + // LINQ serializes XML DTD declaration with an explicit empty 'internal subset' + // (a pair of square brackets at the end of Doctype declaration). + // Even though this is valid XML, XCode does not like it, hence this workaround. + internal static string CleanDtdToString(XDocument doc) + { + // LINQ does not support changing the DTD of existing XDocument instances, + // so we create a dummy document for printing of the Doctype declaration. + // A single dummy element is added to force LINQ not to omit the declaration. + // Also, utf-8 encoding is forced since this is the encoding we use when writing to file in UpdateInfoPlist. + if (doc.DocumentType != null) + { + XDocument tmpDoc = + new XDocument(new XDeclaration("1.0", "utf-8", null), + new XDocumentType(doc.DocumentType.Name, doc.DocumentType.PublicId, doc.DocumentType.SystemId, null), + new XElement(doc.Root.Name)); + return "" + tmpDoc.Declaration + "\n" + tmpDoc.DocumentType + "\n" + doc.Root; + } + else + { + XDocument tmpDoc = new XDocument(new XDeclaration("1.0", "utf-8", null), new XElement(doc.Root.Name)); + return "" + tmpDoc.Declaration + Environment.NewLine + doc.Root; + } + } + + private static string GetText(XElement xml) + { + return String.Join("", xml.Nodes().OfType().Select(x => x.Value).ToArray()); + } + + private static PlistElement ReadElement(XElement xml) + { + switch (xml.Name.LocalName) + { + case "dict": + { + List children = xml.Elements().ToList(); + var el = new PlistElementDict(); + + if (children.Count % 2 == 1) + throw new Exception("Malformed plist file"); + + for (int i = 0; i < children.Count - 1; i++) + { + if (children[i].Name != "key") + throw new Exception("Malformed plist file"); + string key = GetText(children[i]).Trim(); + var newChild = ReadElement(children[i+1]); + if (newChild != null) + { + i++; + el[key] = newChild; + } + } + return el; + } + case "array": + { + List children = xml.Elements().ToList(); + var el = new PlistElementArray(); + + foreach (var childXml in children) + { + var newChild = ReadElement(childXml); + if (newChild != null) + el.values.Add(newChild); + } + return el; + } + case "string": + return new PlistElementString(GetText(xml)); + case "integer": + { + int r; + if (int.TryParse(GetText(xml), out r)) + return new PlistElementInteger(r); + return null; + } + case "true": + return new PlistElementBoolean(true); + case "false": + return new PlistElementBoolean(false); + default: + return null; + } + } + + public void Create() + { + const string doc = "" + + "" + + "" + + "" + + "" + + ""; + ReadFromString(doc); + } + + public void ReadFromFile(string path) + { + ReadFromString(File.ReadAllText(path)); + } + + public void ReadFromStream(TextReader tr) + { + ReadFromString(tr.ReadToEnd()); + } + + public void ReadFromString(string text) + { + XDocument doc = ParseXmlNoDtd(text); + version = (string) doc.Root.Attribute("version"); + XElement xml = doc.XPathSelectElement("plist/dict"); + + var dict = ReadElement(xml); + if (dict == null) + throw new Exception("Error parsing plist file"); + root = dict as PlistElementDict; + if (root == null) + throw new Exception("Malformed plist file"); + } + + private static XElement WriteElement(PlistElement el) + { + if (el is PlistElementBoolean) + { + var realEl = el as PlistElementBoolean; + return new XElement(realEl.value ? "true" : "false"); + } + if (el is PlistElementInteger) + { + var realEl = el as PlistElementInteger; + return new XElement("integer", realEl.value.ToString()); + } + if (el is PlistElementString) + { + var realEl = el as PlistElementString; + return new XElement("string", realEl.value); + } + if (el is PlistElementDict) + { + var realEl = el as PlistElementDict; + var dictXml = new XElement("dict"); + foreach (var kv in realEl.values) + { + var keyXml = new XElement("key", kv.Key); + var valueXml = WriteElement(kv.Value); + if (valueXml != null) + { + dictXml.Add(keyXml); + dictXml.Add(valueXml); + } + } + return dictXml; + } + if (el is PlistElementArray) + { + var realEl = el as PlistElementArray; + var arrayXml = new XElement("array"); + foreach (var v in realEl.values) + { + var elXml = WriteElement(v); + if (elXml != null) + arrayXml.Add(elXml); + } + return arrayXml; + } + return null; + } + + public void WriteToFile(string path) + { + System.Text.Encoding utf8WithoutBom = new System.Text.UTF8Encoding(false); + File.WriteAllText(path, WriteToString(), utf8WithoutBom); + } + + public void WriteToStream(TextWriter tw) + { + tw.Write(WriteToString()); + } + + public string WriteToString() + { + var el = WriteElement(root); + var rootEl = new XElement("plist"); + rootEl.Add(new XAttribute("version", version)); + rootEl.Add(el); + + var doc = new XDocument(); + doc.Add(rootEl); + return CleanDtdToString(doc).Replace("\r\n", "\n"); + } + } + +} // namespace UnityEditor.iOS.XCode diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/PlistParser.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/PlistParser.cs.meta new file mode 100644 index 0000000..91dded4 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/PlistParser.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: c2ed9743fe9634c808ea1dc01c20e01e +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/ProjectCapabilityManager.cs b/Assets/Appirater/NativeLocale/Editor/Xcode/ProjectCapabilityManager.cs new file mode 100644 index 0000000..f4acc5b --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/ProjectCapabilityManager.cs @@ -0,0 +1,578 @@ +using System; +using System.IO; + +namespace ChillyRoom.UnityEditor.iOS.Xcode +{ + // This class is here to help you add capabilities to your Xcode project. + // Because capabilities modify the PBXProject, the entitlements file and/or the Info.plist and not consistently, + // it can be tedious. + // Therefore this class open the PBXProject that is always modify by capabilities and open Entitlement and info.plist only when needed. + // For optimisation reasons, we write the file only in the close method. + // If you don't call it the file will not be written. + public class ProjectCapabilityManager + { + private readonly string m_BuildPath; + private readonly string m_TargetGuid; + private readonly string m_PBXProjectPath; + private readonly string m_EntitlementFilePath; + private PlistDocument m_Entitlements; + private PlistDocument m_InfoPlist; + protected internal PBXProject project; + + // Create the manager with the required parameter to open files and set the properties in the write place. + public ProjectCapabilityManager(string pbxProjectPath, string entitlementFilePath, string targetName) + { + m_BuildPath = Directory.GetParent(Path.GetDirectoryName(pbxProjectPath)).FullName; + + m_EntitlementFilePath = entitlementFilePath; + m_PBXProjectPath = pbxProjectPath; + project = new PBXProject(); + project.ReadFromString(File.ReadAllText(m_PBXProjectPath)); + m_TargetGuid = project.TargetGuidByName(targetName); + } + + // Write the actual file to the disk. + // If you don't call this method nothing will change. + public void WriteToFile() + { + File.WriteAllText(m_PBXProjectPath, project.WriteToString()); + if (m_Entitlements != null) + m_Entitlements.WriteToFile(PBXPath.Combine(m_BuildPath, m_EntitlementFilePath)); + if (m_InfoPlist != null) + m_InfoPlist.WriteToFile(PBXPath.Combine(m_BuildPath, "Info.plist")); + } + + // Add the iCloud capability with the desired options. + public void AddiCloud(bool keyValueStorage, bool iCloudDocument, string[] customContainers) + { + var ent = GetOrCreateEntitlementDoc(); + var val = (ent.root[ICloudEntitlements.ContainerIdValue] = new PlistElementArray()) as PlistElementArray; + if (iCloudDocument) + { + val.values.Add(new PlistElementString(ICloudEntitlements.ContainerIdValue)); + var ser = (ent.root[ICloudEntitlements.ServicesKey] = new PlistElementArray()) as PlistElementArray; + ser.values.Add(new PlistElementString(ICloudEntitlements.ServicesKitValue)); + ser.values.Add(new PlistElementString(ICloudEntitlements.ServicesDocValue)); + var ubiquity = (ent.root[ICloudEntitlements.UbiquityContainerIdKey] = new PlistElementArray()) as PlistElementArray; + ubiquity.values.Add(new PlistElementString(ICloudEntitlements.UbiquityContainerIdValue)); + for (var i = 0; i < customContainers.Length; i++) + { + ser.values.Add(new PlistElementString(customContainers[i])); + } + } + + if (keyValueStorage) + { + ent.root[ICloudEntitlements.KeyValueStoreKey] = new PlistElementString(ICloudEntitlements.KeyValueStoreValue); + } + + project.AddCapability(m_TargetGuid, PBXCapabilityType.iCloud, m_EntitlementFilePath, iCloudDocument); + } + + // Add Push (or remote) Notifications capability to your project + public void AddPushNotifications(bool development) + { + GetOrCreateEntitlementDoc().root[PushNotificationEntitlements.Key] = new PlistElementString(development ? PushNotificationEntitlements.DevelopmentValue : PushNotificationEntitlements.ProductionValue); + project.AddCapability(m_TargetGuid, PBXCapabilityType.PushNotifications, m_EntitlementFilePath); + } + + // Add GameCenter capability to the project. + public void AddGameCenter() + { + var arr = (GetOrCreateInfoDoc().root[GameCenterInfo.Key] ?? (GetOrCreateInfoDoc().root[GameCenterInfo.Key] = new PlistElementArray())) as PlistElementArray; + arr.values.Add(new PlistElementString(GameCenterInfo.Value)); + project.AddCapability(m_TargetGuid, PBXCapabilityType.GameCenter); + } + + // Add Wallet capability to the project. + public void AddWallet(string[] passSubset) + { + var arr = (GetOrCreateEntitlementDoc().root[WalletEntitlements.Key] = new PlistElementArray()) as PlistElementArray; + if ((passSubset == null || passSubset.Length == 0) && arr != null) + { + arr.values.Add(new PlistElementString(WalletEntitlements.BaseValue + WalletEntitlements.BaseValue)); + } + else + { + for (var i = 0; i < passSubset.Length; i++) + { + if (arr != null) + arr.values.Add(new PlistElementString(WalletEntitlements.BaseValue + passSubset[i])); + } + } + + project.AddCapability(m_TargetGuid, PBXCapabilityType.Wallet, m_EntitlementFilePath); + } + + // Add Siri capability to the project. + public void AddSiri() + { + GetOrCreateEntitlementDoc().root[SiriEntitlements.Key] = new PlistElementBoolean(true); + + project.AddCapability(m_TargetGuid, PBXCapabilityType.Siri, m_EntitlementFilePath); + } + + // Add Apple Pay capability to the project. + public void AddApplePay(string[] merchants) + { + var arr = (GetOrCreateEntitlementDoc().root[ApplePayEntitlements.Key] = new PlistElementArray()) as PlistElementArray; + for (var i = 0; i < merchants.Length; i++) + { + arr.values.Add(new PlistElementString(merchants[i])); + } + + project.AddCapability(m_TargetGuid, PBXCapabilityType.ApplePay, m_EntitlementFilePath); + } + + // Add In App Purchase capability to the project. + public void AddInAppPurchase() + { + project.AddCapability(m_TargetGuid, PBXCapabilityType.InAppPurchase); + } + + // Add Maps capability to the project. + public void AddMaps(MapsOptions options) + { + var bundleArr = (GetOrCreateInfoDoc().root[MapsInfo.BundleKey] ?? (GetOrCreateInfoDoc().root[MapsInfo.BundleKey] = new PlistElementArray())) as PlistElementArray; + bundleArr.values.Add(new PlistElementDict()); + PlistElementDict bundleDic = GetOrCreateUniqueDictElementInArray(bundleArr); + bundleDic[MapsInfo.BundleNameKey] = new PlistElementString(MapsInfo.BundleNameValue); + var bundleTypeArr = (bundleDic[MapsInfo.BundleTypeKey] ?? (bundleDic[MapsInfo.BundleTypeKey] = new PlistElementArray())) as PlistElementArray; + GetOrCreateStringElementInArray(bundleTypeArr, MapsInfo.BundleTypeValue); + + var optionArr = (GetOrCreateInfoDoc().root[MapsInfo.ModeKey] ?? + (GetOrCreateInfoDoc().root[MapsInfo.ModeKey] = new PlistElementArray())) as PlistElementArray; + if ((options & MapsOptions.Airplane) == MapsOptions.Airplane) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModePlaneValue); + } + if ((options & MapsOptions.Bike) == MapsOptions.Bike) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeBikeValue); + } + if ((options & MapsOptions.Bus) == MapsOptions.Bus) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeBusValue); + } + if ((options & MapsOptions.Car) == MapsOptions.Car) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeCarValue); + } + if ((options & MapsOptions.Ferry) == MapsOptions.Ferry) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeFerryValue); + } + if ((options & MapsOptions.Other) == MapsOptions.Other) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeOtherValue); + } + if ((options & MapsOptions.Pedestrian) == MapsOptions.Pedestrian) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModePedestrianValue); + } + if ((options & MapsOptions.RideSharing) == MapsOptions.RideSharing) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeRideShareValue); + } + if ((options & MapsOptions.StreetCar) == MapsOptions.StreetCar) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeStreetCarValue); + } + if ((options & MapsOptions.Subway) == MapsOptions.Subway) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeSubwayValue); + } + if ((options & MapsOptions.Taxi) == MapsOptions.Taxi) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeTaxiValue); + } + if ((options & MapsOptions.Train) == MapsOptions.Train) + { + GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeTrainValue); + } + + project.AddCapability(m_TargetGuid, PBXCapabilityType.Maps); + } + + // Add Personal VPN capability to the project. + public void AddPersonalVPN() + { + var arr = (GetOrCreateEntitlementDoc().root[VPNEntitlements.Key] = new PlistElementArray()) as PlistElementArray; + arr.values.Add(new PlistElementString(VPNEntitlements.Value)); + + project.AddCapability(m_TargetGuid, PBXCapabilityType.PersonalVPN, m_EntitlementFilePath); + } + + // Add Background capability to the project with the options wanted. + public void AddBackgroundModes(BackgroundModesOptions options) + { + var optionArr = (GetOrCreateInfoDoc().root[BackgroundInfo.Key] ?? + (GetOrCreateInfoDoc().root[BackgroundInfo.Key] = new PlistElementArray())) as PlistElementArray; + + if ((options & BackgroundModesOptions.ActsAsABluetoothLEAccessory) == BackgroundModesOptions.ActsAsABluetoothLEAccessory) + { + GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeActsBluetoothValue); + } + if ((options & BackgroundModesOptions.AudioAirplayPiP) == BackgroundModesOptions.AudioAirplayPiP) + { + GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeAudioValue); + } + if ((options & BackgroundModesOptions.BackgroundFetch) == BackgroundModesOptions.BackgroundFetch) + { + GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeFetchValue); + } + if ((options & BackgroundModesOptions.ExternalAccessoryCommunication) == BackgroundModesOptions.ExternalAccessoryCommunication) + { + GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeExtAccessoryValue); + } + if ((options & BackgroundModesOptions.LocationUpdates) == BackgroundModesOptions.LocationUpdates) + { + GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeLocationValue); + } + if ((options & BackgroundModesOptions.NewsstandDownloads) == BackgroundModesOptions.NewsstandDownloads) + { + GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeNewsstandValue); + } + if ((options & BackgroundModesOptions.RemoteNotifications) == BackgroundModesOptions.RemoteNotifications) + { + GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModePushValue); + } + if ((options & BackgroundModesOptions.VoiceOverIP) == BackgroundModesOptions.VoiceOverIP) + { + GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeVOIPValue); + } + project.AddCapability(m_TargetGuid, PBXCapabilityType.BackgroundModes); + } + + // Add Keychain Sharing capability to the project with a list of groups. + public void AddKeychainSharing(string[] accessGroups) + { + var arr = (GetOrCreateEntitlementDoc().root[KeyChainEntitlements.Key] = new PlistElementArray()) as PlistElementArray; + if (accessGroups != null) + { + for (var i = 0; i < accessGroups.Length; i++) + { + arr.values.Add(new PlistElementString(accessGroups[i])); + } + } + else + { + arr.values.Add(new PlistElementString(KeyChainEntitlements.DefaultValue)); + } + + project.AddCapability(m_TargetGuid, PBXCapabilityType.KeychainSharing, m_EntitlementFilePath); + } + + // Add Inter App Audio capability to the project. + public void AddInterAppAudio() + { + GetOrCreateEntitlementDoc().root[AudioEntitlements.Key] = new PlistElementBoolean(true); + project.AddCapability(m_TargetGuid, PBXCapabilityType.InterAppAudio, m_EntitlementFilePath); + } + + // Add Associated Domains capability to the project. + public void AddAssociatedDomains(string[] domains) + { + var arr = (GetOrCreateEntitlementDoc().root[AssociatedDomainsEntitlements.Key] = new PlistElementArray()) as PlistElementArray; + for (var i = 0; i < domains.Length; i++) + { + arr.values.Add(new PlistElementString(domains[i])); + } + + project.AddCapability(m_TargetGuid, PBXCapabilityType.AssociatedDomains, m_EntitlementFilePath); + } + + // Add App Groups capability to the project. + public void AddAppGroups(string[] groups) + { + var arr = (GetOrCreateEntitlementDoc().root[AppGroupsEntitlements.Key] = new PlistElementArray()) as PlistElementArray; + for (var i = 0; i < groups.Length; i++) + { + arr.values.Add(new PlistElementString(groups[i])); + } + + project.AddCapability(m_TargetGuid, PBXCapabilityType.AppGroups, m_EntitlementFilePath); + } + + // Add HomeKit capability to the project. + public void AddHomeKit() + { + GetOrCreateEntitlementDoc().root[HomeKitEntitlements.Key] = new PlistElementBoolean(true); + project.AddCapability(m_TargetGuid, PBXCapabilityType.HomeKit, m_EntitlementFilePath); + } + + // Add Data Protection capability to the project. + public void AddDataProtection() + { + GetOrCreateEntitlementDoc().root[DataProtectionEntitlements.Key] = new PlistElementString(DataProtectionEntitlements.Value); + project.AddCapability(m_TargetGuid, PBXCapabilityType.DataProtection, m_EntitlementFilePath); + } + + // Add HealthKit capability to the project. + public void AddHealthKit() + { + var capabilityArr = (GetOrCreateInfoDoc().root[HealthInfo.Key] ?? + (GetOrCreateInfoDoc().root[HealthInfo.Key] = new PlistElementArray())) as PlistElementArray; + GetOrCreateStringElementInArray(capabilityArr, HealthInfo.Value); + GetOrCreateEntitlementDoc().root[HealthKitEntitlements.Key] = new PlistElementBoolean(true); + project.AddCapability(m_TargetGuid, PBXCapabilityType.HealthKit, m_EntitlementFilePath); + } + + // Add Wireless Accessory Configuration capability to the project. + public void AddWirelessAccessoryConfiguration() + { + GetOrCreateEntitlementDoc().root[WirelessAccessoryConfigurationEntitlements.Key] = new PlistElementBoolean(true); + project.AddCapability(m_TargetGuid, PBXCapabilityType.WirelessAccessoryConfiguration, m_EntitlementFilePath); + } + + private PlistDocument GetOrCreateEntitlementDoc() + { + if (m_Entitlements == null) + { + m_Entitlements = new PlistDocument(); + string[] entitlementsFiles = Directory.GetFiles(m_BuildPath, m_EntitlementFilePath); + if (entitlementsFiles.Length > 0) + { + m_Entitlements.ReadFromFile(entitlementsFiles[0]); + } + else + { + m_Entitlements.Create(); + } + } + + return m_Entitlements; + } + + private PlistDocument GetOrCreateInfoDoc() + { + if (m_InfoPlist == null) + { + m_InfoPlist = new PlistDocument(); + string[] infoFiles = Directory.GetFiles(m_BuildPath + "/", "Info.plist"); + if (infoFiles.Length > 0) + { + m_InfoPlist.ReadFromFile(infoFiles[0]); + } + else + { + m_InfoPlist.Create(); + } + } + + return m_InfoPlist; + } + + private PlistElementString GetOrCreateStringElementInArray(PlistElementArray root, string value) + { + PlistElementString r = null; + var c = root.values.Count; + var exist = false; + for (var i = 0; i < c; i++) + { + if (root.values[i] is PlistElementString && (root.values[i] as PlistElementString).value == value) + { + r = root.values[i] as PlistElementString; + exist = true; + } + } + if (!exist) + { + r = new PlistElementString(value); + root.values.Add(r); + } + return r; + } + + private PlistElementDict GetOrCreateUniqueDictElementInArray(PlistElementArray root) + { + PlistElementDict r; + if (root.values.Count == 0) + { + r = root.values[0] as PlistElementDict; + } + else + { + r = new PlistElementDict(); + root.values.Add(r); + } + return r; + } + } + + // The list of options available for Background Mode. + [Flags] + [Serializable] + public enum BackgroundModesOptions + { + None = 0, + AudioAirplayPiP = 1<<0, + LocationUpdates = 1<<1, + VoiceOverIP = 1<<2, + NewsstandDownloads = 1<<3, + ExternalAccessoryCommunication = 1<<4, + UsesBluetoothLEAccessory = 1<<5, + ActsAsABluetoothLEAccessory = 1<<6, + BackgroundFetch = 1<<7, + RemoteNotifications = 1<<8 + } + + // The list of options available for Maps. + [Serializable] + [Flags] + public enum MapsOptions + { + None = 0, + Airplane = 1<<0, + Bike = 1<<1, + Bus = 1<<2, + Car = 1<<3, + Ferry = 1<<4, + Pedestrian = 1<<5, + RideSharing = 1<<6, + StreetCar = 1<<7, + Subway = 1<<8, + Taxi = 1<<9, + Train = 1<<10, + Other = 1<<11 + } + + /* Follows the large quantity of string used as key and value all over the place in the info.plist or entitlements file. */ + internal class GameCenterInfo + { + internal static readonly string Key = "UIRequiredDeviceCapabilities"; + internal static readonly string Value = "gamekit"; + } + + internal class MapsInfo + { + internal static readonly string BundleKey = "CFBundleDocumentTypes"; + internal static readonly string BundleNameKey = "CFBundleTypeName"; + internal static readonly string BundleNameValue = "MKDirectionsRequest"; + internal static readonly string BundleTypeKey = "LSItemContentTypes"; + internal static readonly string BundleTypeValue = "com.apple.maps.directionsrequest"; + internal static readonly string ModeKey = "MKDirectionsApplicationSupportedModes"; + internal static readonly string ModePlaneValue = "MKDirectionsModePlane"; + internal static readonly string ModeBikeValue = "MKDirectionsModeBike"; + internal static readonly string ModeBusValue = "MKDirectionsModeBus"; + internal static readonly string ModeCarValue = "MKDirectionsModeCar"; + internal static readonly string ModeFerryValue = "MKDirectionsModeFerry"; + internal static readonly string ModeOtherValue = "MKDirectionsModeOther"; + internal static readonly string ModePedestrianValue = "MKDirectionsModePedestrian"; + internal static readonly string ModeRideShareValue = "MKDirectionsModeRideShare"; + internal static readonly string ModeStreetCarValue = "MKDirectionsModeStreetCar"; + internal static readonly string ModeSubwayValue = "MKDirectionsModeSubway"; + internal static readonly string ModeTaxiValue = "MKDirectionsModeTaxi"; + internal static readonly string ModeTrainValue = "MKDirectionsModeTrain"; + } + + internal class BackgroundInfo + { + internal static readonly string Key = "UIBackgroundModes"; + internal static readonly string ModeAudioValue = "audio"; + internal static readonly string ModeBluetoothValue = "bluetooth-central"; + internal static readonly string ModeActsBluetoothValue = "bluetooth-peripheral"; + internal static readonly string ModeExtAccessoryValue = "external-accessory"; + internal static readonly string ModeFetchValue = "fetch"; + internal static readonly string ModeLocationValue = "location"; + internal static readonly string ModeNewsstandValue = "newsstand-content"; + internal static readonly string ModePushValue = "remote-notification"; + internal static readonly string ModeVOIPValue = "voip"; + } + + internal class HealthInfo + { + internal static readonly string Key = "UIRequiredDeviceCapabilities"; + internal static readonly string Value = "healthkit"; + } + + internal class ICloudEntitlements + { + internal static readonly string ContainerIdKey = "com.apple.developer.icloud-container-identifiers"; + internal static readonly string UbiquityContainerIdKey = "com.apple.developer.ubiquity-container-identifiers"; + internal static readonly string ContainerIdValue = "iCloud.$(CFBundleIdentifier)"; + internal static readonly string UbiquityContainerIdValue = "iCloud.$(CFBundleIdentifier)"; + internal static readonly string ServicesKey = "com.apple.developer.icloud-services"; + internal static readonly string ServicesDocValue = "CloudDocuments"; + internal static readonly string ServicesKitValue = "CloudKit"; + internal static readonly string KeyValueStoreKey = "com.apple.developer.ubiquity-kvstore-identifier"; + internal static readonly string KeyValueStoreValue = "$(TeamIdentifierPrefix)$(CFBundleIdentifier)"; + } + + internal class PushNotificationEntitlements + { + internal static readonly string Key = "aps-environment"; + internal static readonly string DevelopmentValue = "development"; + internal static readonly string ProductionValue = "production"; + } + + internal class WalletEntitlements + { + internal static readonly string Key = "com.apple.developer.pass-type-identifiers"; + internal static readonly string BaseValue = "$(TeamIdentifierPrefix)"; + internal static readonly string DefaultValue = "*"; + } + + internal class SiriEntitlements + { + internal static readonly string Key = "com.apple.developer.siri"; + } + + internal class ApplePayEntitlements + { + internal static readonly string Key = "com.apple.developer.in-app-payments"; + } + + internal class VPNEntitlements + { + internal static readonly string Key = "com.apple.developer.networking.vpn.api"; + internal static readonly string Value = "allow-vpn"; + } + + internal class KeyChainEntitlements + { + internal static readonly string Key = "keychain-access-groups"; + internal static readonly string DefaultValue = "$(AppIdentifierPrefix)$(CFBundleIdentifier)"; + } + + internal class AudioEntitlements + { + internal static readonly string Key = "inter-app-audio"; + } + + internal class AssociatedDomainsEntitlements + { + // value is an array of string of domains + internal static readonly string Key = "com.apple.developer.associated-domains"; + } + + internal class AppGroupsEntitlements + { + // value is an array of string of groups + internal static readonly string Key = "com.apple.security.application-groups"; + } + + internal class HomeKitEntitlements + { + // value is bool true. + internal static readonly string Key = "com.apple.developer.homekit"; + } + + internal class DataProtectionEntitlements + { + internal static readonly string Key = "com.apple.developer.default-data-protection"; + internal static readonly string Value = "NSFileProtectionComplete"; + } + + internal class HealthKitEntitlements + { + // value is bool true. + internal static readonly string Key = "com.apple.developer.healthkit"; + } + + internal class WirelessAccessoryConfigurationEntitlements + { + // value is bool true. + internal static readonly string Key = "com.apple.external-accessory.wireless-configuration"; + } +} diff --git a/Assets/Appirater/NativeLocale/Editor/Xcode/ProjectCapabilityManager.cs.meta b/Assets/Appirater/NativeLocale/Editor/Xcode/ProjectCapabilityManager.cs.meta new file mode 100644 index 0000000..65aa9a0 --- /dev/null +++ b/Assets/Appirater/NativeLocale/Editor/Xcode/ProjectCapabilityManager.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 2bfeebf480ee141979c8981b78c0a488 +timeCreated: 1496741691 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/Test.meta b/Assets/Appirater/Test.meta new file mode 100644 index 0000000..31ff9b5 --- /dev/null +++ b/Assets/Appirater/Test.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 06e0aa0e354934454a95ef0dd494513f +folderAsset: yes +timeCreated: 1511996344 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/Test/AppiraterTestScene.unity b/Assets/Appirater/Test/AppiraterTestScene.unity new file mode 100644 index 0000000..be2e527 Binary files /dev/null and b/Assets/Appirater/Test/AppiraterTestScene.unity differ diff --git a/Assets/Appirater/Test/AppiraterTestScene.unity.meta b/Assets/Appirater/Test/AppiraterTestScene.unity.meta new file mode 100644 index 0000000..2b9405d --- /dev/null +++ b/Assets/Appirater/Test/AppiraterTestScene.unity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f86b947956d3b456e8736ed6b418e613 +timeCreated: 1511996358 +licenseType: Free +DefaultImporter: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Appirater/Test/MainController.cs b/Assets/Appirater/Test/MainController.cs new file mode 100644 index 0000000..73a3a2f --- /dev/null +++ b/Assets/Appirater/Test/MainController.cs @@ -0,0 +1,28 @@ +using UnityEngine; +using UnityEngine.UI; + + +namespace Appirater.Test +{ + public class MainController : MonoBehaviour { + public string AppId = "552035781"; + public int DaysUntilPrompt = 1; + public int UsesUntilPrompt = 10; + public int SignificantEventsUntilPrompt = -1; + public int TimeBeforeReminding = 2; + public bool Debug = true; + + // Use this for initialization + void Start () { + InitAppirater(); + GameObject.Find("InitButton").GetComponent