From 625e2b79348bec9b46c5661cbf811b58a432087b Mon Sep 17 00:00:00 2001 From: CaiYanPeng Date: Fri, 10 Dec 2021 18:10:20 +0800 Subject: [PATCH] =?UTF-8?q?app=E5=90=8D=E7=A7=B0=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=8C=96=E8=87=AA=E9=80=82=E5=BA=94=EF=BC=9B=E5=88=86=E4=BA=AB?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Editor/RadarChartInspector.cs | 2 +- Assets/Editor/Locale.meta | 8 + Assets/Editor/Locale/en.lproj.meta | 8 + .../Editor/Locale/en.lproj/InfoPlist.strings | 9 + .../Locale/en.lproj/InfoPlist.strings.meta | 7 + Assets/Editor/Locale/zh-Hans.lproj.meta | 8 + .../Locale/zh-Hans.lproj/InfoPlist.strings | 9 + .../zh-Hans.lproj/InfoPlist.strings.meta | 7 + Assets/Editor/NativeLocale.cs | 77 + Assets/Editor/NativeLocale.cs.meta | 11 + Assets/Editor/XCodePostProcessBuild.cs | 2 + Assets/Editor/Xcode.meta | 8 + Assets/Editor/Xcode/AssetCatalog.cs | 813 +++++++++ Assets/Editor/Xcode/AssetCatalog.cs.meta | 12 + Assets/Editor/Xcode/JsonParser.cs | 257 +++ Assets/Editor/Xcode/JsonParser.cs.meta | 12 + Assets/Editor/Xcode/PBX.meta | 9 + Assets/Editor/Xcode/PBX/Elements.cs | 106 ++ Assets/Editor/Xcode/PBX/Elements.cs.meta | 12 + Assets/Editor/Xcode/PBX/Lexer.cs | 243 +++ Assets/Editor/Xcode/PBX/Lexer.cs.meta | 12 + Assets/Editor/Xcode/PBX/Objects.cs | 1007 +++++++++++ Assets/Editor/Xcode/PBX/Objects.cs.meta | 12 + Assets/Editor/Xcode/PBX/Parser.cs | 171 ++ Assets/Editor/Xcode/PBX/Parser.cs.meta | 12 + Assets/Editor/Xcode/PBX/Sections.cs | 122 ++ Assets/Editor/Xcode/PBX/Sections.cs.meta | 12 + Assets/Editor/Xcode/PBX/Serializer.cs | 259 +++ Assets/Editor/Xcode/PBX/Serializer.cs.meta | 12 + Assets/Editor/Xcode/PBX/Utils.cs | 300 ++++ Assets/Editor/Xcode/PBX/Utils.cs.meta | 12 + Assets/Editor/Xcode/PBXCapabilityType.cs | 125 ++ Assets/Editor/Xcode/PBXCapabilityType.cs.meta | 12 + Assets/Editor/Xcode/PBXPath.cs | 106 ++ Assets/Editor/Xcode/PBXPath.cs.meta | 12 + Assets/Editor/Xcode/PBXProject.cs | 1538 +++++++++++++++++ Assets/Editor/Xcode/PBXProject.cs.meta | 12 + Assets/Editor/Xcode/PBXProjectData.cs | 713 ++++++++ Assets/Editor/Xcode/PBXProjectData.cs.meta | 12 + Assets/Editor/Xcode/PBXProjectExtensions.cs | 296 ++++ .../Editor/Xcode/PBXProjectExtensions.cs.meta | 12 + Assets/Editor/Xcode/PlistParser.cs | 351 ++++ Assets/Editor/Xcode/PlistParser.cs.meta | 12 + .../Editor/Xcode/ProjectCapabilityManager.cs | 578 +++++++ .../Xcode/ProjectCapabilityManager.cs.meta | 12 + Assets/Plugins/Android/AndroidManifest.xml | 7 +- .../Plugins/Android/ImageSelector-release.aar | Bin 20719 -> 0 bytes .../Android/PowerFunAndroidPlugin-release.aar | Bin 0 -> 78422 bytes ...=> PowerFunAndroidPlugin-release.aar.meta} | 2 +- .../Android/baseProjectTemplate.gradle | 13 + Assets/Plugins/Android/mainTemplate.gradle | 28 +- Assets/Plugins/Android/res.meta | 8 + Assets/Plugins/Android/res/values-zh.meta | 8 + .../Plugins/Android/res/values-zh/strings.xml | 5 + .../Android/res/values-zh/strings.xml.meta | 7 + Assets/Plugins/Android/res/values.meta | 8 + Assets/Plugins/Android/res/values/strings.xml | 5 + .../Android/res/values/strings.xml.meta | 7 + Assets/Plugins/iOS/WechatNativeBridge.m | 7 +- .../UI/Prefab/Ride/Mobile/Panel.prefab | 624 +++---- Assets/Scripts/App.cs | 2 +- Assets/Scripts/Mobile/WeChatController.cs | 11 + .../Scenes/Ride/Scripts/CyclingController.cs | 14 +- .../Scenes/Ride/Scripts/ResultPanelScript.cs | 11 + ProjectSettings/GraphicsSettings.asset | 1 + ProjectSettings/ProjectSettings.asset | 4 +- 66 files changed, 7772 insertions(+), 342 deletions(-) create mode 100644 Assets/Editor/Locale.meta create mode 100644 Assets/Editor/Locale/en.lproj.meta create mode 100644 Assets/Editor/Locale/en.lproj/InfoPlist.strings create mode 100644 Assets/Editor/Locale/en.lproj/InfoPlist.strings.meta create mode 100644 Assets/Editor/Locale/zh-Hans.lproj.meta create mode 100644 Assets/Editor/Locale/zh-Hans.lproj/InfoPlist.strings create mode 100644 Assets/Editor/Locale/zh-Hans.lproj/InfoPlist.strings.meta create mode 100644 Assets/Editor/NativeLocale.cs create mode 100644 Assets/Editor/NativeLocale.cs.meta create mode 100644 Assets/Editor/Xcode.meta create mode 100644 Assets/Editor/Xcode/AssetCatalog.cs create mode 100644 Assets/Editor/Xcode/AssetCatalog.cs.meta create mode 100644 Assets/Editor/Xcode/JsonParser.cs create mode 100644 Assets/Editor/Xcode/JsonParser.cs.meta create mode 100644 Assets/Editor/Xcode/PBX.meta create mode 100644 Assets/Editor/Xcode/PBX/Elements.cs create mode 100644 Assets/Editor/Xcode/PBX/Elements.cs.meta create mode 100644 Assets/Editor/Xcode/PBX/Lexer.cs create mode 100644 Assets/Editor/Xcode/PBX/Lexer.cs.meta create mode 100644 Assets/Editor/Xcode/PBX/Objects.cs create mode 100644 Assets/Editor/Xcode/PBX/Objects.cs.meta create mode 100644 Assets/Editor/Xcode/PBX/Parser.cs create mode 100644 Assets/Editor/Xcode/PBX/Parser.cs.meta create mode 100644 Assets/Editor/Xcode/PBX/Sections.cs create mode 100644 Assets/Editor/Xcode/PBX/Sections.cs.meta create mode 100644 Assets/Editor/Xcode/PBX/Serializer.cs create mode 100644 Assets/Editor/Xcode/PBX/Serializer.cs.meta create mode 100644 Assets/Editor/Xcode/PBX/Utils.cs create mode 100644 Assets/Editor/Xcode/PBX/Utils.cs.meta create mode 100644 Assets/Editor/Xcode/PBXCapabilityType.cs create mode 100644 Assets/Editor/Xcode/PBXCapabilityType.cs.meta create mode 100644 Assets/Editor/Xcode/PBXPath.cs create mode 100644 Assets/Editor/Xcode/PBXPath.cs.meta create mode 100644 Assets/Editor/Xcode/PBXProject.cs create mode 100644 Assets/Editor/Xcode/PBXProject.cs.meta create mode 100644 Assets/Editor/Xcode/PBXProjectData.cs create mode 100644 Assets/Editor/Xcode/PBXProjectData.cs.meta create mode 100644 Assets/Editor/Xcode/PBXProjectExtensions.cs create mode 100644 Assets/Editor/Xcode/PBXProjectExtensions.cs.meta create mode 100644 Assets/Editor/Xcode/PlistParser.cs create mode 100644 Assets/Editor/Xcode/PlistParser.cs.meta create mode 100644 Assets/Editor/Xcode/ProjectCapabilityManager.cs create mode 100644 Assets/Editor/Xcode/ProjectCapabilityManager.cs.meta delete mode 100644 Assets/Plugins/Android/ImageSelector-release.aar create mode 100644 Assets/Plugins/Android/PowerFunAndroidPlugin-release.aar rename Assets/Plugins/Android/{ImageSelector-release.aar.meta => PowerFunAndroidPlugin-release.aar.meta} (93%) create mode 100644 Assets/Plugins/Android/res.meta create mode 100644 Assets/Plugins/Android/res/values-zh.meta create mode 100644 Assets/Plugins/Android/res/values-zh/strings.xml create mode 100644 Assets/Plugins/Android/res/values-zh/strings.xml.meta create mode 100644 Assets/Plugins/Android/res/values.meta create mode 100644 Assets/Plugins/Android/res/values/strings.xml create mode 100644 Assets/Plugins/Android/res/values/strings.xml.meta diff --git a/Assets/Chart And Graph/Editor/RadarChartInspector.cs b/Assets/Chart And Graph/Editor/RadarChartInspector.cs index bb88a279..266d1f44 100644 --- a/Assets/Chart And Graph/Editor/RadarChartInspector.cs +++ b/Assets/Chart And Graph/Editor/RadarChartInspector.cs @@ -10,7 +10,7 @@ using UnityEngine; namespace Assets { [CustomEditor(typeof(RadarChart), true)] - class RadarChartInspector : Editor + class RadarChartInspector : UnityEditor.Editor { bool mCategories = false; bool mGroups = false; diff --git a/Assets/Editor/Locale.meta b/Assets/Editor/Locale.meta new file mode 100644 index 00000000..e443a4f5 --- /dev/null +++ b/Assets/Editor/Locale.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a5024d7258021714e832782d73a13749 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Locale/en.lproj.meta b/Assets/Editor/Locale/en.lproj.meta new file mode 100644 index 00000000..0643b0ce --- /dev/null +++ b/Assets/Editor/Locale/en.lproj.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 85d4f615e53b346478ff4dff569a317f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Locale/en.lproj/InfoPlist.strings b/Assets/Editor/Locale/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..1839ac38 --- /dev/null +++ b/Assets/Editor/Locale/en.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* + InfoPlist.strings + IosToRN + + Created by lishuo on 2021/2/24. + +*/ +"CFBundleName"="PowerFun"; +"CFBundleDisplayName"="PowerFun"; diff --git a/Assets/Editor/Locale/en.lproj/InfoPlist.strings.meta b/Assets/Editor/Locale/en.lproj/InfoPlist.strings.meta new file mode 100644 index 00000000..0b290cb9 --- /dev/null +++ b/Assets/Editor/Locale/en.lproj/InfoPlist.strings.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a4aa11356c3e11a41941fc03fae3a26b +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Locale/zh-Hans.lproj.meta b/Assets/Editor/Locale/zh-Hans.lproj.meta new file mode 100644 index 00000000..2636eef5 --- /dev/null +++ b/Assets/Editor/Locale/zh-Hans.lproj.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cde742ac5daeb784ca7837d4e2622f26 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Locale/zh-Hans.lproj/InfoPlist.strings b/Assets/Editor/Locale/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 00000000..e73c0ff4 --- /dev/null +++ b/Assets/Editor/Locale/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* + InfoPlist.strings + IosToRN + + Created by lishuo on 2021/2/24. + +*/ +"CFBundleName"="运动地球"; +"CFBundleDisplayName"="运动地球"; diff --git a/Assets/Editor/Locale/zh-Hans.lproj/InfoPlist.strings.meta b/Assets/Editor/Locale/zh-Hans.lproj/InfoPlist.strings.meta new file mode 100644 index 00000000..5298980d --- /dev/null +++ b/Assets/Editor/Locale/zh-Hans.lproj/InfoPlist.strings.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d407327a982cae2468c2e1c372b5dbd3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/NativeLocale.cs b/Assets/Editor/NativeLocale.cs new file mode 100644 index 00000000..06732cf7 --- /dev/null +++ b/Assets/Editor/NativeLocale.cs @@ -0,0 +1,77 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using System.IO; +using ChillyRoom.UnityEditor.iOS.Xcode; +namespace Assets +{ + 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"); + DirectoryCopy(src, Path.Combine(projectPath, "Unity-iPhone/" + locale + ".lproj")); + + string fileRelatvePath = string.Format("Unity-iPhone/{0}.lproj/Localizable.strings", locale); + proj.AddLocalization("Localizable.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/Editor/NativeLocale.cs.meta b/Assets/Editor/NativeLocale.cs.meta new file mode 100644 index 00000000..0c98ead4 --- /dev/null +++ b/Assets/Editor/NativeLocale.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 04d272fbec5a52a4a8a1bb6b201a3030 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/XCodePostProcessBuild.cs b/Assets/Editor/XCodePostProcessBuild.cs index e5a90aea..a55f99ba 100644 --- a/Assets/Editor/XCodePostProcessBuild.cs +++ b/Assets/Editor/XCodePostProcessBuild.cs @@ -5,6 +5,7 @@ using System.IO; using UnityEngine; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; + #endif public static class XCodePostProcessBuild @@ -25,6 +26,7 @@ public static class XCodePostProcessBuild SetFrameworksAndBuildSettings(projectPath); SetInfoList(pathToBuiltProject, "com.ZhiXingPai.PowerFun", "wxe3573a84e7e29902"); SetAssociatedDomains(projectPath, "wx.powerfun.com.cn"); + Assets.NativeLocale.AddLocalizedStringsIOS(pathToBuiltProject, Path.Combine(Application.dataPath, "Editor/Locale")); } private static void SetFrameworksAndBuildSettings(string path) diff --git a/Assets/Editor/Xcode.meta b/Assets/Editor/Xcode.meta new file mode 100644 index 00000000..8338e00f --- /dev/null +++ b/Assets/Editor/Xcode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9b05453b42e5aa40b0b080cdec35b95 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/Xcode/AssetCatalog.cs b/Assets/Editor/Xcode/AssetCatalog.cs new file mode 100644 index 00000000..588758d1 --- /dev/null +++ b/Assets/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/Editor/Xcode/AssetCatalog.cs.meta b/Assets/Editor/Xcode/AssetCatalog.cs.meta new file mode 100644 index 00000000..31465df4 --- /dev/null +++ b/Assets/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/Editor/Xcode/JsonParser.cs b/Assets/Editor/Xcode/JsonParser.cs new file mode 100644 index 00000000..b54ba98d --- /dev/null +++ b/Assets/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/Editor/Xcode/JsonParser.cs.meta b/Assets/Editor/Xcode/JsonParser.cs.meta new file mode 100644 index 00000000..d325bf83 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX.meta b/Assets/Editor/Xcode/PBX.meta new file mode 100644 index 00000000..cd39a5d9 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Elements.cs b/Assets/Editor/Xcode/PBX/Elements.cs new file mode 100644 index 00000000..5be13a97 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Elements.cs.meta b/Assets/Editor/Xcode/PBX/Elements.cs.meta new file mode 100644 index 00000000..b757deab --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Lexer.cs b/Assets/Editor/Xcode/PBX/Lexer.cs new file mode 100644 index 00000000..c7c320b5 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Lexer.cs.meta b/Assets/Editor/Xcode/PBX/Lexer.cs.meta new file mode 100644 index 00000000..9a985d8e --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Objects.cs b/Assets/Editor/Xcode/PBX/Objects.cs new file mode 100644 index 00000000..3b790f7d --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Objects.cs.meta b/Assets/Editor/Xcode/PBX/Objects.cs.meta new file mode 100644 index 00000000..69ebfd15 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Parser.cs b/Assets/Editor/Xcode/PBX/Parser.cs new file mode 100644 index 00000000..7f1a1bb9 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Parser.cs.meta b/Assets/Editor/Xcode/PBX/Parser.cs.meta new file mode 100644 index 00000000..115422cd --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Sections.cs b/Assets/Editor/Xcode/PBX/Sections.cs new file mode 100644 index 00000000..20ddcc39 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Sections.cs.meta b/Assets/Editor/Xcode/PBX/Sections.cs.meta new file mode 100644 index 00000000..aadf820d --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Serializer.cs b/Assets/Editor/Xcode/PBX/Serializer.cs new file mode 100644 index 00000000..df0ea396 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Serializer.cs.meta b/Assets/Editor/Xcode/PBX/Serializer.cs.meta new file mode 100644 index 00000000..43a2fe90 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Utils.cs b/Assets/Editor/Xcode/PBX/Utils.cs new file mode 100644 index 00000000..6753c10d --- /dev/null +++ b/Assets/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/Editor/Xcode/PBX/Utils.cs.meta b/Assets/Editor/Xcode/PBX/Utils.cs.meta new file mode 100644 index 00000000..0dec2b9b --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXCapabilityType.cs b/Assets/Editor/Xcode/PBXCapabilityType.cs new file mode 100644 index 00000000..bf1382f0 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXCapabilityType.cs.meta b/Assets/Editor/Xcode/PBXCapabilityType.cs.meta new file mode 100644 index 00000000..a9fdc80f --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXPath.cs b/Assets/Editor/Xcode/PBXPath.cs new file mode 100644 index 00000000..821c8bc5 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXPath.cs.meta b/Assets/Editor/Xcode/PBXPath.cs.meta new file mode 100644 index 00000000..cba89f0f --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXProject.cs b/Assets/Editor/Xcode/PBXProject.cs new file mode 100644 index 00000000..7fb04b17 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXProject.cs.meta b/Assets/Editor/Xcode/PBXProject.cs.meta new file mode 100644 index 00000000..5d35d557 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXProjectData.cs b/Assets/Editor/Xcode/PBXProjectData.cs new file mode 100644 index 00000000..d0db1c2b --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXProjectData.cs.meta b/Assets/Editor/Xcode/PBXProjectData.cs.meta new file mode 100644 index 00000000..182f43a2 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXProjectExtensions.cs b/Assets/Editor/Xcode/PBXProjectExtensions.cs new file mode 100644 index 00000000..e6497a33 --- /dev/null +++ b/Assets/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/Editor/Xcode/PBXProjectExtensions.cs.meta b/Assets/Editor/Xcode/PBXProjectExtensions.cs.meta new file mode 100644 index 00000000..6309b076 --- /dev/null +++ b/Assets/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/Editor/Xcode/PlistParser.cs b/Assets/Editor/Xcode/PlistParser.cs new file mode 100644 index 00000000..0f325abb --- /dev/null +++ b/Assets/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/Editor/Xcode/PlistParser.cs.meta b/Assets/Editor/Xcode/PlistParser.cs.meta new file mode 100644 index 00000000..91dded48 --- /dev/null +++ b/Assets/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/Editor/Xcode/ProjectCapabilityManager.cs b/Assets/Editor/Xcode/ProjectCapabilityManager.cs new file mode 100644 index 00000000..f4acc5b6 --- /dev/null +++ b/Assets/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/Editor/Xcode/ProjectCapabilityManager.cs.meta b/Assets/Editor/Xcode/ProjectCapabilityManager.cs.meta new file mode 100644 index 00000000..65aa9a06 --- /dev/null +++ b/Assets/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/Plugins/Android/AndroidManifest.xml b/Assets/Plugins/Android/AndroidManifest.xml index b44cf951..f4ec1f05 100644 --- a/Assets/Plugins/Android/AndroidManifest.xml +++ b/Assets/Plugins/Android/AndroidManifest.xml @@ -20,10 +20,9 @@ - - + diff --git a/Assets/Plugins/Android/ImageSelector-release.aar b/Assets/Plugins/Android/ImageSelector-release.aar deleted file mode 100644 index 2c133d0d585b4cc350a85e2fc1ee64fddec3439e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20719 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 SU9{I1{;S=DG_UCj=>GsOwgfQ% literal 0 HcmV?d00001 diff --git a/Assets/Plugins/Android/ImageSelector-release.aar.meta b/Assets/Plugins/Android/PowerFunAndroidPlugin-release.aar.meta similarity index 93% rename from Assets/Plugins/Android/ImageSelector-release.aar.meta rename to Assets/Plugins/Android/PowerFunAndroidPlugin-release.aar.meta index 7c6cd9ba..a4d49356 100644 --- a/Assets/Plugins/Android/ImageSelector-release.aar.meta +++ b/Assets/Plugins/Android/PowerFunAndroidPlugin-release.aar.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 9722f205596f2fd499991ea299119cc8 +guid: c2b78b518d3971e4a89095c3e33ddc00 PluginImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Plugins/Android/baseProjectTemplate.gradle b/Assets/Plugins/Android/baseProjectTemplate.gradle index 942b13b8..88443a96 100644 --- a/Assets/Plugins/Android/baseProjectTemplate.gradle +++ b/Assets/Plugins/Android/baseProjectTemplate.gradle @@ -20,6 +20,19 @@ allprojects { repositories {**ARTIFACTORYREPOSITORY** google() jcenter() + maven { + url 'https://api.mapbox.com/downloads/v2/releases/maven' + authentication { + basic(BasicAuthentication) + } + credentials { + // Do not change the username below. + // This should always be `mapbox` (not your username). + username = 'mapbox' + // Use the secret token you stored in gradle.properties as the password + password = "sk.eyJ1IjoidGFsZXNmYW4iLCJhIjoiY2t3ZDg4bmV4NDFubjJucm9nMDZsNnFyZyJ9._ukjbjSdzXFsF-4rZ1sPeA" + } + } flatDir { dirs "${project(':unityLibrary').projectDir}/libs" } diff --git a/Assets/Plugins/Android/mainTemplate.gradle b/Assets/Plugins/Android/mainTemplate.gradle index 3f2ba978..5f5626cf 100644 --- a/Assets/Plugins/Android/mainTemplate.gradle +++ b/Assets/Plugins/Android/mainTemplate.gradle @@ -4,20 +4,13 @@ apply plugin: 'com.android.library' **APPLY_PLUGINS** dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation(name: 'animated-vector-drawable-25.1.0', ext:'aar') - implementation(name: 'appcompat-v7-25.1.0', ext:'aar') - implementation(name: 'com.mapbox.android.unity-debug', ext:'aar') - implementation(name: 'libcore-release', ext:'aar') - implementation(name: 'libtelemetry-full-release', ext:'aar') - implementation(name: 'support-compat-25.1.0', ext:'aar') - implementation(name: 'support-core-ui-25.1.0', ext:'aar') - implementation(name: 'support-core-utils-25.1.0', ext:'aar') - implementation(name: 'support-media-compat-25.1.0', ext:'aar') - implementation(name: 'support-v4-25.1.0', ext:'aar') - implementation(name: 'support-vector-drawable-25.1.0', ext:'aar') implementation(name: 'UnityCallWechatShare-release', ext:'aar') - implementation files ('libs/ImageSelector-release.aar') + implementation files ("libs/unity-classes.jar") + implementation files ("libs/unityandroidbluetoothlelib.jar") + implementation files ('libs/PowerFunAndroidPlugin-release.aar') + implementation ('com.mapbox.maps:android:10.2.0-beta.1'){ + exclude group: 'group_name', module: 'module_name' + } } android { @@ -49,3 +42,12 @@ android { }**PACKAGING_OPTIONS** }**REPOSITORIES****SOURCE_BUILD_SETUP** **EXTERNAL_SOURCES** + +task localizeAppName(type: Copy) { + from("${project.rootDir}/unityLibrary/unity-android-resources/res/") { + include "**/strings.xml" + } + into "${project.rootDir}/launcher/src/main/res" +} + +preBuild.dependsOn(localizeAppName) \ No newline at end of file diff --git a/Assets/Plugins/Android/res.meta b/Assets/Plugins/Android/res.meta new file mode 100644 index 00000000..34354e87 --- /dev/null +++ b/Assets/Plugins/Android/res.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 503cb92f2ff04994295fb1ef0fb6ff87 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/res/values-zh.meta b/Assets/Plugins/Android/res/values-zh.meta new file mode 100644 index 00000000..34a16709 --- /dev/null +++ b/Assets/Plugins/Android/res/values-zh.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b1a4f8ddbb19f5149a54b3952a1b1e1d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/res/values-zh/strings.xml b/Assets/Plugins/Android/res/values-zh/strings.xml new file mode 100644 index 00000000..a36e7e26 --- /dev/null +++ b/Assets/Plugins/Android/res/values-zh/strings.xml @@ -0,0 +1,5 @@ + + + 运动地球 + Game view + \ No newline at end of file diff --git a/Assets/Plugins/Android/res/values-zh/strings.xml.meta b/Assets/Plugins/Android/res/values-zh/strings.xml.meta new file mode 100644 index 00000000..ec358817 --- /dev/null +++ b/Assets/Plugins/Android/res/values-zh/strings.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ae90b72fae98b7d409fef80a77ef58dd +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/res/values.meta b/Assets/Plugins/Android/res/values.meta new file mode 100644 index 00000000..3d1cf0c2 --- /dev/null +++ b/Assets/Plugins/Android/res/values.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6ac3db33836fa754384a431594aaea4a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Android/res/values/strings.xml b/Assets/Plugins/Android/res/values/strings.xml new file mode 100644 index 00000000..d7be3f45 --- /dev/null +++ b/Assets/Plugins/Android/res/values/strings.xml @@ -0,0 +1,5 @@ + + + PowerFun + Game view + \ No newline at end of file diff --git a/Assets/Plugins/Android/res/values/strings.xml.meta b/Assets/Plugins/Android/res/values/strings.xml.meta new file mode 100644 index 00000000..5ea6ccf5 --- /dev/null +++ b/Assets/Plugins/Android/res/values/strings.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 269366f50793f6c479888c9eecf16ea1 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/iOS/WechatNativeBridge.m b/Assets/Plugins/iOS/WechatNativeBridge.m index f279a2ab..049b7e04 100644 --- a/Assets/Plugins/iOS/WechatNativeBridge.m +++ b/Assets/Plugins/iOS/WechatNativeBridge.m @@ -30,12 +30,12 @@ void LaunchMiniProgram(const char* miniProgramID, const char* path){ } // 分享图片至微信 -void ShareImgToWX(int scene, UInt8 *msgByteArrayData, int arrayLength, const char* title, const char* description){ +void ShareImgToWX(int scene, UInt8 *msgByteArrayData, int arrayLength){ WXImageObject *imageObject = [WXImageObject object]; imageObject.imageData = [[NSData alloc] initWithBytes:msgByteArrayData length:arrayLength]; WXMediaMessage *message = [WXMediaMessage message]; - message.title = [NSString stringWithUTF8String:title]; - message.description = [NSString stringWithUTF8String:description]; + // message.title = [NSString stringWithUTF8String:title]; + // message.description = [NSString stringWithUTF8String:description]; // 用APP的Icon做缩略图 NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary]; NSString *icon = [[infoPlist valueForKeyPath:@"CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles"] lastObject]; @@ -67,7 +67,6 @@ void ShareUrlToWX(int scene, const char* url, const char* title, const char* des [WXApi sendReq:req completion:nil]; } - //判断是否安装微信 bool IsWechatInstalled_iOS() { diff --git a/Assets/Resources/UI/Prefab/Ride/Mobile/Panel.prefab b/Assets/Resources/UI/Prefab/Ride/Mobile/Panel.prefab index a54e84ca..0dd32c85 100644 --- a/Assets/Resources/UI/Prefab/Ride/Mobile/Panel.prefab +++ b/Assets/Resources/UI/Prefab/Ride/Mobile/Panel.prefab @@ -293,84 +293,6 @@ MonoBehaviour: m_VerticalOverflow: 0 m_LineSpacing: 1 m_Text: ---- !u!1 &437213691946889435 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 2982792994629127324} - - component: {fileID: 6892152824926409725} - - component: {fileID: 7099515369382083389} - - component: {fileID: 7234636846379503339} - m_Layer: 5 - m_Name: Lines - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &2982792994629127324 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 437213691946889435} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 1662293206147616003} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 0, y: 0} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &6892152824926409725 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 437213691946889435} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!222 &7099515369382083389 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 437213691946889435} - m_CullTransparentMesh: 0 ---- !u!114 &7234636846379503339 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 437213691946889435} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 2032ee9ddbfbfb74da66a209b05d468d, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - Thickness: 4 --- !u!1 &561507438981283975 GameObject: m_ObjectHideFlags: 0 @@ -8834,7 +8756,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 0 + m_IsActive: 1 --- !u!224 &765892905500667676 RectTransform: m_ObjectHideFlags: 0 @@ -25458,7 +25380,7 @@ RectTransform: m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: - - {fileID: 6705637700361051074} + - {fileID: 287130843534808004} - {fileID: 5572785103298013575} m_Father: {fileID: 765892906030958096} m_RootOrder: 3 @@ -28401,6 +28323,84 @@ MonoBehaviour: m_ChildControlHeight: 0 m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 +--- !u!1 &4561237107004918660 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6368931264969813662} + - component: {fileID: 1645432988067376672} + - component: {fileID: 8985708638791463984} + - component: {fileID: 3907800697780269774} + m_Layer: 5 + m_Name: Lines + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6368931264969813662 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4561237107004918660} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 4972958166963389385} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1645432988067376672 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4561237107004918660} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!222 &8985708638791463984 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4561237107004918660} + m_CullTransparentMesh: 0 +--- !u!114 &3907800697780269774 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 4561237107004918660} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2032ee9ddbfbfb74da66a209b05d468d, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + Thickness: 2 --- !u!1 &4805486912667576755 GameObject: m_ObjectHideFlags: 0 @@ -28853,109 +28853,6 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: radius: 30 ---- !u!1 &5219485531322787435 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1662293206147616003} - - component: {fileID: 1339745422673456410} - - component: {fileID: 996780360449557400} - - component: {fileID: 5245709930834093469} - - component: {fileID: 4133168441936445500} - m_Layer: 5 - m_Name: RectMask(Clone) - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1662293206147616003 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5219485531322787435} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: - - {fileID: 2982792994629127324} - - {fileID: 1715951100218252328} - m_Father: {fileID: 6705637700361051074} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} - m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 0, y: 50} - m_SizeDelta: {x: 500, y: 50} - m_Pivot: {x: 0, y: 1} ---- !u!114 &1339745422673456410 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5219485531322787435} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} - m_Name: - m_EditorClassIdentifier: - m_ShowMaskGraphic: 0 ---- !u!222 &996780360449557400 -CanvasRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5219485531322787435} - m_CullTransparentMesh: 0 ---- !u!114 &5245709930834093469 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5219485531322787435} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Material: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 - m_Maskable: 1 - m_OnCullStateChanged: - m_PersistentCalls: - m_Calls: [] - m_Sprite: {fileID: 10917, guid: 0000000000000000f000000000000000, type: 0} - m_Type: 1 - m_PreserveAspect: 0 - m_FillCenter: 1 - m_FillMethod: 4 - m_FillAmount: 1 - m_FillClockwise: 1 - m_FillOrigin: 0 - m_UseSpriteMesh: 0 - m_PixelsPerUnitMultiplier: 1 ---- !u!114 &4133168441936445500 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 5219485531322787435} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &5227404737329208808 GameObject: m_ObjectHideFlags: 0 @@ -29914,7 +29811,7 @@ MonoBehaviour: m_VerticalOverflow: 0 m_LineSpacing: 1 m_Text: 0 ---- !u!1 &6731657110001096442 +--- !u!1 &6761719125807787846 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -29922,10 +29819,73 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 1715951100218252328} - - component: {fileID: 2606228147842712072} - - component: {fileID: 4489143448244481427} - - component: {fileID: 9169757412572267114} + - component: {fileID: 2921649382181662335} + - component: {fileID: 4686157667057369641} + - component: {fileID: 1501255716731572845} + m_Layer: 5 + m_Name: textController + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2921649382181662335 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6761719125807787846} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 287130843534808004} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -250, y: -25} + m_SizeDelta: {x: 100, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &4686157667057369641 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6761719125807787846} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &1501255716731572845 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6761719125807787846} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f8c8bf670921e114bbea10f451c86392, type: 3} + m_Name: + m_EditorClassIdentifier: + Camera: {fileID: 0} + PlaneDistance: 20 +--- !u!1 &6867828174471998518 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6669230693244627319} + - component: {fileID: 6880249551563795014} + - component: {fileID: 2688744985322922845} + - component: {fileID: 1028417144058143333} m_Layer: 5 m_Name: Lines m_TagString: Untagged @@ -29933,52 +29893,52 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!224 &1715951100218252328 +--- !u!224 &6669230693244627319 RectTransform: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6731657110001096442} + m_GameObject: {fileID: 6867828174471998518} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_Children: [] - m_Father: {fileID: 1662293206147616003} - m_RootOrder: 1 + m_Father: {fileID: 4972958166963389385} + m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &2606228147842712072 +--- !u!114 &6880249551563795014 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6731657110001096442} + m_GameObject: {fileID: 6867828174471998518} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!222 &4489143448244481427 +--- !u!222 &2688744985322922845 CanvasRenderer: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6731657110001096442} + m_GameObject: {fileID: 6867828174471998518} m_CullTransparentMesh: 0 ---- !u!114 &9169757412572267114 +--- !u!114 &1028417144058143333 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6731657110001096442} + m_GameObject: {fileID: 6867828174471998518} m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 2032ee9ddbfbfb74da66a209b05d468d, type: 3} @@ -29991,57 +29951,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - Thickness: 2 ---- !u!1 &6790952765333462635 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 6705637700361051074} - - component: {fileID: 5625754829217035930} - m_Layer: 0 - m_Name: New Game Object - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &6705637700361051074 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6790952765333462635} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 0.96, y: 0.96, z: 1} - m_Children: - - {fileID: 1662293206147616003} - - {fileID: 4455722854794813142} - m_Father: {fileID: 2877296740073624297} - m_RootOrder: 0 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0.5, y: 0.5} - m_AnchorMax: {x: 0.5, y: 0.5} - m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 500, y: 50} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &5625754829217035930 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6790952765333462635} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} - m_Name: - m_EditorClassIdentifier: + Thickness: 4 --- !u!1 &6885174058740936923 GameObject: m_ObjectHideFlags: 0 @@ -30135,69 +30045,6 @@ MonoBehaviour: m_EditorClassIdentifier: m_HorizontalFit: 2 m_VerticalFit: 0 ---- !u!1 &6905895147960617872 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 4455722854794813142} - - component: {fileID: 6000514500222502934} - - component: {fileID: 5031116583493021538} - m_Layer: 5 - m_Name: textController - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &4455722854794813142 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6905895147960617872} - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_Children: [] - m_Father: {fileID: 6705637700361051074} - m_RootOrder: 1 - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0.5, y: 0.5} - m_AnchorMax: {x: 0.5, y: 0.5} - m_AnchoredPosition: {x: -250, y: -25} - m_SizeDelta: {x: 100, y: 100} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &6000514500222502934 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6905895147960617872} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &5031116583493021538 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6905895147960617872} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f8c8bf670921e114bbea10f451c86392, type: 3} - m_Name: - m_EditorClassIdentifier: - Camera: {fileID: 0} - PlaneDistance: 20 --- !u!1 &7128831287110258232 GameObject: m_ObjectHideFlags: 0 @@ -32198,6 +32045,109 @@ MonoBehaviour: m_ChildControlHeight: 0 m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 +--- !u!1 &7743152534115465197 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4972958166963389385} + - component: {fileID: 2462729567061685641} + - component: {fileID: 6932560375282435178} + - component: {fileID: 1204421178344973644} + - component: {fileID: 7276889366372022131} + m_Layer: 5 + m_Name: RectMask(Clone) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4972958166963389385 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7743152534115465197} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 6669230693244627319} + - {fileID: 6368931264969813662} + m_Father: {fileID: 287130843534808004} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 50} + m_SizeDelta: {x: 500, y: 50} + m_Pivot: {x: 0, y: 1} +--- !u!114 &2462729567061685641 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7743152534115465197} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_ShowMaskGraphic: 0 +--- !u!222 &6932560375282435178 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7743152534115465197} + m_CullTransparentMesh: 0 +--- !u!114 &1204421178344973644 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7743152534115465197} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10917, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &7276889366372022131 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7743152534115465197} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1 &8084747437690518146 GameObject: m_ObjectHideFlags: 0 @@ -33473,6 +33423,56 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: key: +--- !u!1 &8870311823578833747 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 287130843534808004} + - component: {fileID: 3040018482647644442} + m_Layer: 0 + m_Name: New Game Object + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &287130843534808004 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8870311823578833747} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0.96, y: 0.96, z: 1} + m_Children: + - {fileID: 4972958166963389385} + - {fileID: 2921649382181662335} + m_Father: {fileID: 2877296740073624297} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 500, y: 50} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &3040018482647644442 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8870311823578833747} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 55b5a59897c650342a9b23ff348a9992, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1 &9118781381026562979 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/App.cs b/Assets/Scripts/App.cs index 718402a0..0b620809 100644 --- a/Assets/Scripts/App.cs +++ b/Assets/Scripts/App.cs @@ -18,7 +18,7 @@ public delegate void ChangeLanguageDelegate(); public static class App { - public static string Host = "http://192.168.0.101:5084/"; + public static string Host = "http://192.168.0.101:5087/"; public static string AppVersion = Application.version; diff --git a/Assets/Scripts/Mobile/WeChatController.cs b/Assets/Scripts/Mobile/WeChatController.cs index 239cd65f..5ecb9d7d 100644 --- a/Assets/Scripts/Mobile/WeChatController.cs +++ b/Assets/Scripts/Mobile/WeChatController.cs @@ -27,6 +27,9 @@ public class WeChatController [DllImport("__Internal")] private static extern bool checkLocation(); + + [DllImport("__Internal")] + private static extern void ShareImgToWX(int scene, byte[] msgByteArrayData, int arrayLength); #endif #endregion /// @@ -128,6 +131,14 @@ public class WeChatController { mainActivityClass.CallStatic("ShareImageToWX", scene, ImageToBytes(image)); } + public void ShareImageToWX(int scene, byte[] image) + { +#if UNITY_ANDROID + mainActivityClass.CallStatic("ShareImageToWX", scene, image); +#elif UNITY_IOS + ShareImgToWX(scene,image,image.Length); +#endif + } /// /// 分享音乐至微信 diff --git a/Assets/Scripts/Scenes/Ride/Scripts/CyclingController.cs b/Assets/Scripts/Scenes/Ride/Scripts/CyclingController.cs index 38ceb052..bed32fdc 100644 --- a/Assets/Scripts/Scenes/Ride/Scripts/CyclingController.cs +++ b/Assets/Scripts/Scenes/Ride/Scripts/CyclingController.cs @@ -880,8 +880,11 @@ public class CyclingController : DeviceServiceMonoBase } cyclingController.recorderData.SaveWithLocalRecordAysnc(cyclingModel, selectParamModel, imageFileName, recordId, path); } - - protected void CaptureCamera(Camera camera, Rect rect,string fileName) + public byte[] CaptureCamera() + { + return CaptureCameraReturnByte(Camera.main, new Rect(Screen.width * 0f, Screen.height * 0f, Screen.width * 0.5f, Screen.height * 0.5f)); + } + private byte[] CaptureCameraReturnByte(Camera camera, Rect rect) { // 创建一个RenderTexture对象 RenderTexture rt = new RenderTexture((int)rect.width, (int)rect.height, 0); @@ -905,8 +908,11 @@ public class CyclingController : DeviceServiceMonoBase RenderTexture.active = null; // JC: added to avoid errors GameObject.Destroy(rt); // 最后将这些纹理数据,成一个图片文件 - byte[] bytes = screenShot.EncodeToJPG(); - + return screenShot.EncodeToJPG(); + } + protected void CaptureCamera(Camera camera, Rect rect,string fileName) + { + byte[] bytes = CaptureCameraReturnByte(camera, rect); //var path = Helper.GetDataDir("MapWorkoutRecords/images"); //string filename = path + "/" + Guid.NewGuid().ToString() + ".png"; System.IO.File.WriteAllBytes(fileName, bytes); diff --git a/Assets/Scripts/Scenes/Ride/Scripts/ResultPanelScript.cs b/Assets/Scripts/Scenes/Ride/Scripts/ResultPanelScript.cs index ca863ed9..708514c0 100644 --- a/Assets/Scripts/Scenes/Ride/Scripts/ResultPanelScript.cs +++ b/Assets/Scripts/Scenes/Ride/Scripts/ResultPanelScript.cs @@ -39,6 +39,7 @@ namespace Assets.Scenes.Ride.Scripts #region 功能按钮 GameObject goResultBtn; GameObject cancelBtn; + GameObject shareWxBtn; #endregion CyclingController cyclingController; @@ -67,6 +68,8 @@ namespace Assets.Scenes.Ride.Scripts #endregion goResultBtn = transform.Find("ConFirmButton").gameObject; cancelBtn = transform.Find("CloseButton").gameObject; + shareWxBtn = transform.Find("ToolBarPanel/WeChatButton").gameObject; + } public void InjectController(CyclingController controller) @@ -74,7 +77,15 @@ namespace Assets.Scenes.Ride.Scripts cyclingController = controller; cyclingController.AddEvent(goResultBtn, UnityEngine.EventSystems.EventTriggerType.PointerClick, GoResult); cyclingController.AddEvent(cancelBtn, UnityEngine.EventSystems.EventTriggerType.PointerClick, Cancel); + cyclingController.AddEvent(shareWxBtn, EventTriggerType.PointerClick, shareToWx); } + + private void shareToWx(BaseEventData b) + { + var bs = cyclingController.CaptureCamera(); + WeChatController.Instance.ShareImageToWX(0, bs); + } + private void GoResult(BaseEventData baseEventData) { if (App.MainSceneParam.ContainsKey("Name")) diff --git a/ProjectSettings/GraphicsSettings.asset b/ProjectSettings/GraphicsSettings.asset index 4706883c..9b996cb4 100644 --- a/ProjectSettings/GraphicsSettings.asset +++ b/ProjectSettings/GraphicsSettings.asset @@ -38,6 +38,7 @@ GraphicsSettings: - {fileID: 16000, guid: 0000000000000000f000000000000000, type: 0} - {fileID: 16001, guid: 0000000000000000f000000000000000, type: 0} - {fileID: 17000, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 16003, guid: 0000000000000000f000000000000000, type: 0} m_PreloadedShaders: [] m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 563ec832..4937adc8 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -256,7 +256,7 @@ PlayerSettings: clonedFromGUID: c0afd0d1d80e3634a9dac47e8a0426ea templatePackageId: com.unity.template.3d@4.2.8 templateDefaultScene: Assets/Scenes/SampleScene.unity - AndroidTargetArchitectures: 3 + AndroidTargetArchitectures: 1 AndroidSplashScreenScale: 0 androidSplashScreen: {fileID: 0} AndroidKeystoreName: '{inproject}: Assets/Plugins/Android/powerfun.keystore' @@ -868,7 +868,7 @@ PlayerSettings: platformArchitecture: iPhone: 1 scriptingBackend: - Android: 1 + Android: 0 Standalone: 0 il2cppCompilerConfiguration: Standalone: 0