-
Notifications
You must be signed in to change notification settings - Fork 356
/
Copy pathIOSResolver.cs
2904 lines (2680 loc) · 132 KB
/
IOSResolver.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// <copyright file="VersionHandler.cs" company="Google Inc.">
// Copyright (C) 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#if UNITY_IOS
using Google;
using GooglePlayServices;
using Google.JarResolver;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
namespace Google {
[InitializeOnLoad]
public class IOSResolver : AssetPostprocessor {
/// <summary>
/// Reference to a Cocoapod.
/// </summary>
private class Pod {
/// <summary>
/// Name of the pod.
/// </summary>
public string name = null;
/// <summary>
/// This is a preformatted version expression for pod declarations.
///
/// See: https://guides.cocoapods.org/syntax/podfile.html#pod
/// </summary>
public string version = null;
/// <summary>
/// Properties applied to the pod declaration.
///
/// See:
/// </summary>
public Dictionary<string, string> propertiesByName = new Dictionary<string, string>();
/// <summary>
/// Whether this pod has been compiled with bitcode enabled.
///
/// If any pods are present which have bitcode disabled, bitcode is
/// disabled for an entire project.
/// </summary>
public bool bitcodeEnabled = true;
/// <summary>
/// Additional sources (repositories) to search for this pod.
///
/// Since order is important sources specified in this list
/// are interleaved across each Pod added to the resolver.
/// e.g Pod1.source[0], Pod2.source[0] ...
/// Pod1.source[1], Pod2.source[1] etc.
///
/// See: https://guides.cocoapods.org/syntax/podfile.html#source
/// </summary>
public List<string> sources = new List<string>() {
"https://cdn.cocoapods.org/"
};
/// <summary>
/// Minimum target SDK revision required by this pod.
/// In the form major.minor
/// </summary>
public string minTargetSdk = null;
/// <summary>
/// Whether to add this pod to all targets when multiple
/// </summary>
public bool addToAllTargets = false;
/// <summary>
/// Tag that indicates where this was created.
/// </summary>
public string createdBy = System.Environment.StackTrace;
/// <summary>
/// Whether this pod was read from an XML dependencies file.
/// </summary>
public bool fromXmlFile = false;
/// <summary>
/// Global set of sources for all pods.
/// </summary>
public static List<KeyValuePair<string, string>> Sources =
new List<KeyValuePair<string, string>>();
/// <summary>
/// Convert a dictionary of property key / value pairs to a string that can be appended
/// to a Podfile pod declaration.
/// </summary>
public static string PropertyDictionaryToString(
Dictionary<string, string> propertiesByName) {
if (propertiesByName == null) return "";
var propertyNamesAndValues = new List<string>();
foreach (var propertyItem in propertiesByName) {
propertyNamesAndValues.Add(String.Format(":{0} => {1}", propertyItem.Key,
propertyItem.Value));
}
return String.Join(", ", propertyNamesAndValues.ToArray());
}
/// <summary>
/// Get the path of a pod without quotes. If the path isn't present, returns an empty
/// string.
/// </summary>
public string LocalPath {
get {
string path;
if (!propertiesByName.TryGetValue("path", out path)) return "";
if (path.StartsWith("'") && path.EndsWith("'")) {
path = path.Substring(1, path.Length - 2);
}
return path;
}
}
/// <summary>
/// Format a "pod" line for a Podfile.
/// </summary>
public string PodFilePodLine {
get {
string podLine = String.Format("pod '{0}'", name);
if (!String.IsNullOrEmpty(version)) podLine += String.Format(", '{0}'", version);
var outputPropertiesByName = new Dictionary<string, string>(propertiesByName);
var path = LocalPath;
if (!String.IsNullOrEmpty(path)) {
outputPropertiesByName["path"] = String.Format("'{0}'", Path.GetFullPath(path));
}
var propertiesString = PropertyDictionaryToString(outputPropertiesByName);
if (!String.IsNullOrEmpty(propertiesString)) podLine += ", " + propertiesString;
return podLine;
}
}
/// <summary>
/// Create a pod reference.
/// </summary>
/// <param name="name">Name of the pod.</param>
/// <param name="version">Version of the pod.</param>
/// <param name="bitcodeEnabled">Whether this pod was compiled with
/// bitcode.</param>
/// <param name="minTargetSdk">Minimum target SDK revision required by
/// this pod.</param>
/// <param name="addToAllTargets">Whether to add this pod to all targets when multiple
/// target is supported.</param>
/// <param name="sources">List of sources to search for all pods.
/// Each source is a URL that is injected in the source section of a Podfile
/// See https://guides.cocoapods.org/syntax/podfile.html#source for the description of
/// a source.</param>
/// <param name="propertiesByName">Dictionary of additional properties for the pod
/// reference.</param>
public Pod(string name, string version, bool bitcodeEnabled, string minTargetSdk,
bool addToAllTargets, IEnumerable<string> sources,
Dictionary<string, string> propertiesByName) {
this.name = name;
this.version = version;
if (propertiesByName != null) {
this.propertiesByName = new Dictionary<string, string>(propertiesByName);
}
this.bitcodeEnabled = bitcodeEnabled;
this.minTargetSdk = minTargetSdk;
this.addToAllTargets = addToAllTargets;
if (sources != null) {
var allSources = new List<string>(sources);
allSources.AddRange(this.sources);
this.sources = allSources;
}
}
/// <summary>
/// Convert min target SDK to an integer in the form
/// (major * 10) + minor.
/// </summary>
/// <returns>Numeric minimum SDK revision required by this pod.</returns>
public int MinTargetSdkToVersion() {
string sdkString =
String.IsNullOrEmpty(minTargetSdk) ? "0.0" : minTargetSdk;
if (!sdkString.Contains(".")) {
sdkString = sdkString + ".0";
}
return IOSResolver.TargetSdkStringToVersion(sdkString);
}
/// <summary>
/// Compare with this object.
/// This only compares values that can be encoded into a pod line in a Podfile.
/// </summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>true if both objects have the same contents, false otherwise.</returns>
public override bool Equals(System.Object obj) {
var pod = obj as Pod;
return pod != null &&
name == pod.name &&
version == pod.version &&
propertiesByName.Count == pod.propertiesByName.Count &&
propertiesByName.Keys.All(key =>
pod.propertiesByName.ContainsKey(key) &&
propertiesByName[key] == pod.propertiesByName[key]);
}
/// <summary>
/// Generate a hash of this object.
/// </summary>
/// <returns>Hash of this object.</returns>
public override int GetHashCode() {
int hash = 0;
if (name != null) hash ^= name.GetHashCode();
if (version != null) hash ^= version.GetHashCode();
foreach (var item in propertiesByName) hash ^= item.GetHashCode();
return hash;
}
/// <summary>
/// Given a list of pods bucket them into a dictionary sorted by
/// min SDK version. Pods which specify no minimum version (e.g 0)
/// are ignored.
/// </summary>
/// <param name="pods">Enumerable of pods to query.</param>
/// <returns>Sorted dictionary of lists of pod names bucketed by
/// minimum required SDK version.</returns>
public static SortedDictionary<int, List<string>>
BucketByMinSdkVersion(IEnumerable<Pod> pods) {
var buckets = new SortedDictionary<int, List<string>>();
foreach (var pod in pods) {
int minVersion = pod.MinTargetSdkToVersion();
if (minVersion == 0) {
continue;
}
List<string> nameList = null;
if (!buckets.TryGetValue(minVersion, out nameList)) {
nameList = new List<string>();
}
nameList.Add(pod.name);
buckets[minVersion] = nameList;
}
return buckets;
}
}
private class IOSXmlDependencies : XmlDependencies {
// Properties to parse from a XML pod specification and store in the propert1iesByName
// dictionary of the Pod class. These are eventually expanded to the named arguments of the
// pod declaration in a Podfile.
// The value of each attribute with the exception of "path" is included as-is.
// "path" is converted to a full path on the local filesystem when the Podfile is generated.
private static string[] PODFILE_POD_PROPERTIES = new string[] {
"configurations",
"configuration",
"modular_headers",
"source",
"subspecs",
"path"
};
public IOSXmlDependencies() {
dependencyType = "iOS dependencies";
}
/// <summary>
/// Read XML declared dependencies.
/// </summary>
/// <param name="filename">File to read.</param>
/// <param name="logger">Logger to log with.</param>
///
/// Parses dependencies in the form:
///
/// <dependencies>
/// <iosPods>
/// <iosPod name="name"
/// path="pathToLocal"
/// version="versionSpec"
/// bitcodeEnabled="enabled"
/// minTargetSdk="sdk">
/// <sources>
/// <source>uriToPodSource</source>
/// </sources>
/// </iosPod>
/// </iosPods>
/// </dependencies>
protected override bool Read(string filename, Logger logger) {
IOSResolver.Log(String.Format("Reading iOS dependency XML file {0}", filename),
verbose: true);
var sources = new List<string>();
var trueStrings = new HashSet<string> { "true", "1" };
var falseStrings = new HashSet<string> { "false", "0" };
string podName = null;
string versionSpec = null;
bool bitcodeEnabled = true;
string minTargetSdk = null;
bool addToAllTargets = false;
var propertiesByName = new Dictionary<string, string>();
if (!XmlUtilities.ParseXmlTextFileElements(
filename, logger,
(reader, elementName, isStart, parentElementName, elementNameStack) => {
if (elementName == "dependencies" && parentElementName == "") {
return true;
} else if (elementName == "iosPods" &&
(parentElementName == "dependencies" ||
parentElementName == "")) {
return true;
} else if (elementName == "iosPod" &&
parentElementName == "iosPods") {
if (isStart) {
podName = reader.GetAttribute("name");
propertiesByName = new Dictionary<string, string>();
foreach (var propertyName in PODFILE_POD_PROPERTIES) {
string propertyValue = reader.GetAttribute(propertyName);
if (!String.IsNullOrEmpty(propertyValue)) {
propertiesByName[propertyName] = propertyValue;
}
}
versionSpec = reader.GetAttribute("version");
var bitcodeEnabledString =
(reader.GetAttribute("bitcodeEnabled") ?? "").ToLower();
bitcodeEnabled |= trueStrings.Contains(bitcodeEnabledString);
bitcodeEnabled &= !falseStrings.Contains(bitcodeEnabledString);
var addToAllTargetsString =
(reader.GetAttribute("addToAllTargets") ?? "").ToLower();
addToAllTargets |= trueStrings.Contains(addToAllTargetsString);
addToAllTargets &= !falseStrings.Contains(addToAllTargetsString);
minTargetSdk = reader.GetAttribute("minTargetSdk");
sources = new List<string>();
if (podName == null) {
logger.Log(
String.Format("Pod name not specified while reading {0}:{1}\n",
filename, reader.LineNumber),
level: LogLevel.Warning);
return false;
}
} else {
AddPodInternal(podName, preformattedVersion: versionSpec,
bitcodeEnabled: bitcodeEnabled,
minTargetSdk: minTargetSdk,
addToAllTargets: addToAllTargets,
sources: sources,
overwriteExistingPod: false,
createdBy: String.Format("{0}:{1}",
filename, reader.LineNumber),
fromXmlFile: true,
propertiesByName: propertiesByName);
}
return true;
} else if (elementName == "sources" &&
parentElementName == "iosPod") {
return true;
} else if (elementName == "sources" &&
parentElementName == "iosPods") {
if (isStart) {
sources = new List<string>();
} else {
foreach (var source in sources) {
Pod.Sources.Add(
new KeyValuePair<string, string>(
source, String.Format("{0}:{1}", filename,
reader.LineNumber)));
}
}
return true;
} else if (elementName == "source" &&
parentElementName == "sources") {
if (isStart && reader.Read() && reader.NodeType == XmlNodeType.Text) {
sources.Add(reader.ReadContentAsString());
}
return true;
}
// Ignore unknown tags so that different configurations can be stored in the
// same file.
return true;
})) {
return false;
}
return true;
}
}
// Dictionary of pods to install in the generated Xcode project.
private static SortedDictionary<string, Pod> pods =
new SortedDictionary<string, Pod>();
// Order of post processing operations.
private const int BUILD_ORDER_REFRESH_DEPENDENCIES = 10;
private const int BUILD_ORDER_CHECK_COCOAPODS_INSTALL = 20;
private const int BUILD_ORDER_PATCH_PROJECT = 30;
private const int BUILD_ORDER_GEN_PODFILE = 40;
private const int BUILD_ORDER_INSTALL_PODS = 50;
private const int BUILD_ORDER_UPDATE_DEPS = 60;
// This is appended to the Podfile filename to store a backup of the original Podfile.
// ie. "Podfile_Unity".
private const string UNITY_PODFILE_BACKUP_POSTFIX = "_Unity.backup";
// Installation instructions for the CocoaPods command line tool.
private const string COCOAPOD_INSTALL_INSTRUCTIONS = (
"You can install CocoaPods with the Ruby gem package manager:\n" +
" > sudo gem install -n /usr/local/bin cocoapods\n" +
" > pod setup");
// Dialog box text when the podfile version exceeds the currently configured
// target iOS or tvOS sdk version.
private const string TARGET_SDK_NEEDS_UPDATE_STRING = (
"Target SDK selected in the {0} Player Settings ({1}) is not supported by " +
"the Cocoapods included in this project. The build will very " +
"likely fail. The minimum supported version is \"{2}\" required " +
"by pods ({3})\n" +
"Would you like to update the target SDK version?");
// Dialog box text when the IOSResolver has updated the target sdk version on behalf
// of the user.
private const string UPDATED_TARGET_SDK_STRING = (
"Target {0} SDK has been updated from {1} to {2}. " +
"You must restart the build for this change to take effect.");
// Pod executable filename.
private static string POD_EXECUTABLE = "pod";
// Default paths to search for the "pod" command before falling back to
// querying the Ruby Gem tool for the environment.
private static string[] POD_SEARCH_PATHS = new string[] {
"/usr/local/bin",
"/usr/bin",
"/opt/homebrew/bin",
};
// Ruby Gem executable filename.
private static string GEM_EXECUTABLE = "gem";
/// <summary>
/// Name of the Xcode project generated by Unity.
/// </summary>
public const string PROJECT_NAME = "Unity-iPhone";
/// <summary>
/// Main executable target of the Xcode project generated by Unity before they added support
/// for using Unity as a library or framework. This will be null if it is not supported.
/// </summary>
/// <remarks>This is deprecated, use XcodeTargetNames or GetXcodeTargetGuids instead.</remarks>
public static string TARGET_NAME = null;
// Keys in the editor preferences which control the behavior of this module.
private const string PREFERENCE_NAMESPACE = "Google.IOSResolver.";
// Whether Legacy Cocoapod installation (project level) is enabled.
private const string PREFERENCE_COCOAPODS_INSTALL_ENABLED = PREFERENCE_NAMESPACE + "Enabled";
// Whether Cocoapod uses project files, workspace files, or none (Unity 5.6+ only)
private const string PREFERENCE_COCOAPODS_INTEGRATION_METHOD =
PREFERENCE_NAMESPACE + "CocoapodsIntegrationMethod";
// Whether the Podfile generation is enabled.
private const string PREFERENCE_PODFILE_GENERATION_ENABLED =
PREFERENCE_NAMESPACE + "PodfileEnabled";
// Whether verbose logging is enabled.
private const string PREFERENCE_VERBOSE_LOGGING_ENABLED =
PREFERENCE_NAMESPACE + "VerboseLoggingEnabled";
// Whether execution of the pod tool is performed via the shell.
private const string PREFERENCE_POD_TOOL_EXECUTION_VIA_SHELL_ENABLED =
PREFERENCE_NAMESPACE + "PodToolExecutionViaShellEnabled";
// Whether environment variables should be set when execution is performed via the shell.
private const string PREFERENCE_POD_TOOL_SHELL_EXECUTION_SET_LANG =
PREFERENCE_NAMESPACE + "PodToolShellExecutionSetLang";
// Whether to try to install Cocoapods tools when iOS is selected as the target platform.
private const string PREFERENCE_AUTO_POD_TOOL_INSTALL_IN_EDITOR =
PREFERENCE_NAMESPACE + "AutoPodToolInstallInEditor";
// A nag prompt disabler setting for turning on workspace integration.
private const string PREFERENCE_WARN_UPGRADE_WORKSPACE =
PREFERENCE_NAMESPACE + "UpgradeToWorkspaceWarningDisabled";
// Whether to skip pod install when using workspace integration.
private const string PREFERENCE_SKIP_POD_INSTALL_WHEN_USING_WORKSPACE_INTEGRATION =
PREFERENCE_NAMESPACE + "SkipPodInstallWhenUsingWorkspaceIntegration";
// Whether to add "use_frameworks!" in the Podfile.
private const string PREFERENCE_PODFILE_ADD_USE_FRAMEWORKS =
PREFERENCE_NAMESPACE + "PodfileAddUseFrameworks";
// Whether to statically link framework in the Podfile.
private const string PREFERENCE_PODFILE_STATIC_LINK_FRAMEWORKS =
PREFERENCE_NAMESPACE + "PodfileStaticLinkFrameworks";
// Whether to add Dummy.swift to the generated Xcode project as a workaround to support pods
// with Swift Framework.
private const string PREFERENCE_SWIFT_FRAMEWORK_SUPPORT_WORKAROUND =
PREFERENCE_NAMESPACE + "SwiftFrameworkSupportWorkaroundEnabled";
// The Swift Language Version (SWIFT_VERSION) to update to the generated Xcode Project.
private const string PREFERENCE_SWIFT_LANGUAGE_VERSION =
PREFERENCE_NAMESPACE + "SwiftLanguageVersion";
// Whether to add an main target to Podfile for Unity 2019.3+.
private const string PREFERENCE_PODFILE_ALWAYS_ADD_MAIN_TARGET =
PREFERENCE_NAMESPACE + "PodfileAlwaysAddMainTarget";
// Whether to allow the same pods to be in multiple targets, if specified in Dependecies.xml.
private const string PREFERENCE_PODFILE_ALLOW_PODS_IN_MULTIPLE_TARGETS =
PREFERENCE_NAMESPACE + "PodfileAllowPodsInMultipleTargets";
// List of preference keys, used to restore default settings.
private static string[] PREFERENCE_KEYS = new [] {
PREFERENCE_COCOAPODS_INSTALL_ENABLED,
PREFERENCE_COCOAPODS_INTEGRATION_METHOD,
PREFERENCE_PODFILE_GENERATION_ENABLED,
PREFERENCE_VERBOSE_LOGGING_ENABLED,
PREFERENCE_POD_TOOL_EXECUTION_VIA_SHELL_ENABLED,
PREFERENCE_AUTO_POD_TOOL_INSTALL_IN_EDITOR,
PREFERENCE_WARN_UPGRADE_WORKSPACE,
PREFERENCE_SKIP_POD_INSTALL_WHEN_USING_WORKSPACE_INTEGRATION,
PREFERENCE_PODFILE_ADD_USE_FRAMEWORKS,
PREFERENCE_PODFILE_STATIC_LINK_FRAMEWORKS,
PREFERENCE_SWIFT_FRAMEWORK_SUPPORT_WORKAROUND,
PREFERENCE_SWIFT_LANGUAGE_VERSION,
PREFERENCE_PODFILE_ALWAYS_ADD_MAIN_TARGET,
PREFERENCE_PODFILE_ALLOW_PODS_IN_MULTIPLE_TARGETS
};
// Whether the xcode extension was successfully loaded.
private static bool iOSXcodeExtensionLoaded = true;
// Whether a functioning Cocoapods install is present.
private static bool cocoapodsToolsInstallPresent = false;
private static string IOS_PLAYBACK_ENGINES_PATH =
Path.Combine("PlaybackEngines", "iOSSupport");
// Directory containing downloaded CocoaPods relative to the project
// directory.
private const string PODS_DIR = "Pods";
// Name of the project within PODS_DIR that references downloaded CocoaPods.
private const string PODS_PROJECT_NAME = "Pods";
// Prefix for static library filenames.
private const string LIBRARY_FILENAME_PREFIX = "lib";
// Extension for static library filenames.
private const string LIBRARY_FILENAME_EXTENSION = ".a";
// Pod variable that references the a source pod's root directory which is analogous to the
// Xcode $(SRCROOT) variable.
private const string PODS_VAR_TARGET_SRCROOT = "${PODS_TARGET_SRCROOT}";
// Version of the CocoaPods installation.
private static string podsVersion = "";
private static string PODFILE_GENERATED_COMMENT = "# IOSResolver Generated Podfile";
// Default iOS target SDK if the selected version is invalid.
private const int DEFAULT_IOS_TARGET_SDK = 82;
// Default tvOS target SDK if the selected version is invalid.
private const string DEFAULT_TVOS_TARGET_SDK = "12.0";
// Valid iOS / tvOS target SDK version.
private static Regex TARGET_SDK_REGEX = new Regex("^[0-9]+\\.[0-9]$");
// Current window being used for a long running shell command.
private static CommandLineDialog commandLineDialog = null;
// Mutex for access to commandLineDialog.
private static System.Object commandLineDialogLock = new System.Object();
// Regex that matches a "pod" specification line in a pod file.
// This matches the syntax...
// pod POD_NAME, OPTIONAL_VERSION, :PROPERTY0 => VALUE0 ... , :PROPERTYN => VALUE0
private static Regex PODFILE_POD_REGEX =
new Regex(
// Extract the Cocoapod name and store in the podname group.
@"^\s*pod\s+'(?<podname>[^']+)'\s*" +
// Extract the version field and store in the podversion group.
@"(,\s*'(?<podversion>[^']+)')?" +
// Match the end of the line or a list of property & value pairs.
// Property & value pairs are stored in propertyname / propertyvalue groups.
// Subsequent values are stored in the Captures property of each Group in the order
// they're matched. For example...
// 1 => 2, 3 => 4
// associates Capture objects with values 1, 3 with group "propertyname" and
// Capture objects with values 2, 4 with group "propertyvalue".
@"(|" +
@"(,\s*:(?<propertyname>[^\s]+)\s*=>\s*(" +
// Unquoted property values.
@"(?<propertyvalue>[^\s,]+)\s*|" +
// Quoted string of the form 'foo'.
@"(?<propertyvalue>'[^']+')\s*|" +
// List of the form [1, 2, 3].
@"(?<propertyvalue>\[[^\]]+\])\s*" +
@"))+" +
@")" +
@"$");
// Parses a source URL from a Podfile.
private static Regex PODFILE_SOURCE_REGEX = new Regex(@"^\s*source\s+'([^']*)'");
// Parses comments from a Podfile
private static Regex PODFILE_COMMENT_REGEX = new Regex(@"^\s*\#");
// Parses dependencies from XML dependency files.
private static IOSXmlDependencies xmlDependencies = new IOSXmlDependencies();
// Project level settings for this module.
private static ProjectSettings settings = new ProjectSettings(PREFERENCE_NAMESPACE);
/// <summary>
/// Polls for changes to target iOS SDK version.
/// </summary>
private static PlayServicesResolver.PropertyPoller<string> iosTargetSdkPoller =
new PlayServicesResolver.PropertyPoller<string>("iOS Target SDK");
/// <summary>
/// Polls for changes to target tvOS SDK version.
/// </summary>
private static PlayServicesResolver.PropertyPoller<string> tvosTargetSdkPoller =
new PlayServicesResolver.PropertyPoller<string>("tvOS Target SDK");
// Search for a file up to a maximum search depth stopping the
// depth first search each time the specified file is found.
private static List<string> FindFile(
string searchPath, string fileToFind, int maxDepth,
int currentDepth = 0) {
if (Path.GetFileName(searchPath) == fileToFind) {
return new List<string> { searchPath };
} else if (maxDepth == currentDepth) {
return new List<string>();
}
var foundFiles = new List<string>();
foreach (var file in Directory.GetFiles(searchPath)) {
if (Path.GetFileName(file) == fileToFind) {
foundFiles.Add(file);
}
}
foreach (var dir in Directory.GetDirectories(searchPath)) {
foundFiles.AddRange(FindFile(dir, fileToFind, maxDepth,
currentDepth: currentDepth + 1));
}
return foundFiles;
}
// Try to load the Xcode editor extension.
private static Assembly ResolveUnityEditoriOSXcodeExtension(
object sender, ResolveEventArgs args)
{
// Ignore null assembly references.
if (String.IsNullOrEmpty(args.Name)) return null;
// The UnityEditor.iOS.Extensions.Xcode.dll has the wrong name baked
// into the assembly so references end up resolving as
// Unity.iOS.Extensions.Xcode. Catch this and redirect the load to
// the UnityEditor.iOS.Extensions.Xcode.
string assemblyName;
try {
assemblyName = (new AssemblyName(args.Name)).Name;
} catch (Exception exception) {
// AssemblyName can throw if the DLL isn't found so try falling back to parsing the
// assembly name manually from the fully qualified name.
if (!(exception is FileLoadException ||
exception is IOException)) {
throw exception;
}
assemblyName = args.Name.Split(new [] {','})[0];
}
if (!(assemblyName.Equals("Unity.iOS.Extensions.Xcode") ||
assemblyName.Equals("UnityEditor.iOS.Extensions.Xcode"))) {
return null;
}
Log("Trying to load assembly: " + assemblyName, verbose: true);
iOSXcodeExtensionLoaded = false;
string fixedAssemblyName =
assemblyName.Replace("Unity.", "UnityEditor.") + ".dll";
Log("Redirecting to assembly name: " + fixedAssemblyName,
verbose: true);
// Get the managed DLLs folder.
string folderPath = Path.GetDirectoryName(
Assembly.GetAssembly(
typeof(UnityEditor.AssetPostprocessor)).Location);
// Try searching a common install location.
folderPath = Path.Combine(
(new DirectoryInfo(folderPath)).Parent.FullName,
IOS_PLAYBACK_ENGINES_PATH);
string assemblyPath = Path.Combine(folderPath, fixedAssemblyName);
if (!File.Exists(assemblyPath)) {
string searchPath = (new DirectoryInfo(folderPath)).FullName;
if (UnityEngine.RuntimePlatform.OSXEditor ==
UnityEngine.Application.platform) {
// Unity likes to move their DLLs around between releases to
// keep us on our toes, so search for the DLL under the
// package path.
searchPath = Path.GetDirectoryName(
searchPath.Substring(0, searchPath.LastIndexOf(".app")));
} else {
// Search under the Data directory.
searchPath = Path.GetDirectoryName(
searchPath.Substring(
0, searchPath.LastIndexOf(
"Data" + Path.DirectorySeparatorChar.ToString())));
}
Log("Searching for assembly under " + searchPath, verbose: true);
var files = FindFile(searchPath, fixedAssemblyName, 5);
if (files.Count > 0) assemblyPath = files.ToArray()[0];
}
// Try to load the assembly.
if (!File.Exists(assemblyPath)) {
Log(assemblyPath + " does not exist", verbose: true);
return null;
}
Log("Loading " + assemblyPath, verbose: true);
Assembly assembly = Assembly.LoadFrom(assemblyPath);
if (assembly != null) {
Log("Load succeeded from " + assemblyPath, verbose: true);
iOSXcodeExtensionLoaded = true;
}
return assembly;
}
/// <summary>
/// Initialize the module.
/// </summary>
static IOSResolver() {
// Load log preferences.
UpdateLoggerLevel(VerboseLoggingEnabled);
// NOTE: We can't reference the UnityEditor.iOS.Xcode module in this
// method as the Mono runtime in Unity 4 and below requires all
// dependencies of a method are loaded before the method is executed
// so we install the DLL loader first then try using the Xcode module.
RemapXcodeExtension();
// Delay initialization until the build target is iOS and the editor is not in play
// mode.
EditorInitializer.InitializeOnMainThread(condition: () => {
return (EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS ||
EditorUserBuildSettings.activeBuildTarget == BuildTarget.tvOS) &&
!EditorApplication.isPlayingOrWillChangePlaymode;
}, initializer: Initialize, name: "IOSResolver", logger: logger);
}
/// <summary>
/// Initialize the module. This should be called on the main thread only if
/// current active build target is iOS and not in play mode.
/// </summary>
private static bool Initialize() {
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.iOS &&
EditorUserBuildSettings.activeBuildTarget != BuildTarget.tvOS) {
throw new Exception("IOSResolver.Initialize() is called when active build target " +
"is not iOS+. This should never happen. If it does, please report to the " +
"developer.");
}
// NOTE: It's not possible to catch exceptions a missing reference
// to the UnityEditor.iOS.Xcode assembly in this method as the runtime
// will attempt to load the assembly before the method is executed so
// we handle exceptions here.
try {
InitializeTargetName();
} catch (Exception exception) {
Log("Failed: " + exception.ToString(), level: LogLevel.Error);
if (exception is FileNotFoundException ||
exception is TypeInitializationException ||
exception is TargetInvocationException) {
// It's likely we failed to load the iOS Xcode extension.
Debug.LogWarning(
"Failed to load the " +
"UnityEditor.iOS.Extensions.Xcode dll. " +
"Is iOS support installed?");
} else {
throw exception;
}
}
// If Cocoapod tool auto-installation is enabled try installing on the first update of
// the editor when the editor environment has been initialized.
if (AutoPodToolInstallInEditorEnabled && CocoapodsIntegrationEnabled &&
!ExecutionEnvironment.InBatchMode) {
AutoInstallCocoapods();
}
// Install / remove target SDK property pollers.
SetEnablePollTargetSdks(PodfileGenerationEnabled);
// Load XML dependencies on the next editor update.
if (PodfileGenerationEnabled) {
RefreshXmlDependencies();
}
// Prompt the user to use workspaces if they aren't at least using project level
// integration.
if ((CocoapodsIntegrationMethod)settings.GetInt(PREFERENCE_COCOAPODS_INTEGRATION_METHOD,
CocoapodsIntegrationUpgradeDefault) == CocoapodsIntegrationMethod.None &&
!ExecutionEnvironment.InBatchMode && !UpgradeToWorkspaceWarningDisabled) {
DialogWindow.Display(
"Warning: CocoaPods integration is disabled!",
"Would you like to enable CocoaPods integration with workspaces?\n\n" +
"Unity 5.6+ now supports loading workspaces generated from CocoaPods.\n" +
"If you enable this, and still use Unity less than 5.6, it will fallback " +
"to integrating CocoaPods with the .xcodeproj file.\n",
DialogWindow.Option.Selected0, "Yes", "Not Now", "Silence Warning",
complete: (selectedOption) => {
switch (selectedOption) {
case DialogWindow.Option.Selected0: // Yes
settings.SetInt(PREFERENCE_COCOAPODS_INTEGRATION_METHOD,
(int)CocoapodsIntegrationMethod.Workspace);
break;
case DialogWindow.Option.Selected1: // Not now
break;
case DialogWindow.Option.Selected2: // Ignore
UpgradeToWorkspaceWarningDisabled = true;
break;
}
});
}
return true;
}
/// <summary>
/// Link to the documentation.
/// </summary>
[MenuItem("Assets/External Dependency Manager/iOS Resolver/Documentation")]
public static void OpenDocumentation() {
analytics.OpenUrl(VersionHandlerImpl.DocumentationUrl("#ios-resolver-usage"), "Usage");
}
// Display the iOS resolver settings menu.
[MenuItem("Assets/External Dependency Manager/iOS Resolver/Settings")]
public static void SettingsDialog() {
IOSResolverSettingsDialog window = (IOSResolverSettingsDialog)
EditorWindow.GetWindow(typeof(IOSResolverSettingsDialog), true,
"iOS Resolver Settings");
window.Initialize();
window.Show();
}
/// <summary>
/// Whether Unity only exports a single Xcode build target. This will be true if the current
/// version of Unity supports multiple Xcode build targets, for example if Unity supports use
/// as a library or framework.
/// </summary>
internal static bool MultipleXcodeTargetsSupported {
get {
return typeof(UnityEditor.iOS.Xcode.PBXProject).GetMethod(
"GetUnityMainTargetGuid", Type.EmptyTypes) != null;
}
}
/// <summary>
/// Name of the Xcode main target generated by Unity.
/// </summary>
public static string XcodeMainTargetName {
get {
// NOTE: Unity-iPhone is hard coded in UnityEditor.iOS.Xcode.PBXProject and will no
// longer be exposed via GetUnityTargetName(). It hasn't changed in many years though
// so we'll use this constant as a relatively safe default.
return MultipleXcodeTargetsSupported ? "Unity-iPhone" :
(string)VersionHandler.InvokeStaticMethod(typeof(UnityEditor.iOS.Xcode.PBXProject),
"GetUnityTargetName", null);
}
}
/// <summary>
/// Name of the Xcode UnityFramework target generated by Unity 2019.3+
/// </summary>
public static string XcodeUnityFrameworkTargetName {
get {
return "UnityFramework";
}
}
/// <summary>
/// Name of the Xcode target which contains Unity libraries.
/// From Unity 2019.3+, Unity includes all its libraries and native libraries under Assets
/// folder to 'UnityFramework' instead of 'Unity-iPhone'.
/// </summary>
public static string XcodeTargetWithUnityLibraries {
get {
return MultipleXcodeTargetsSupported ?
XcodeUnityFrameworkTargetName : XcodeMainTargetName;
}
}
/// <summary>
/// Initialize the TARGET_NAME property.
/// This will be "Unity-iPhone" in versions of Unity (2019.3+) that added support for using
/// Unity as a library or framework.
/// </summary>
/// <returns>Name of the Unity Xcode build target.</returns>
private static string InitializeTargetName() {
// NOTE: Unity-iPhone is hard coded in UnityEditor.iOS.Xcode.PBXProject and will soon longer
// be exposed via GetUnityTargetName(). It hasn't changed in many years though so we'll use
// this constant as a relatively safe default for users of the TARGET_NAME variable.
TARGET_NAME = XcodeMainTargetName;
return TARGET_NAME;
}
// Fix loading of the Xcode extension dll.
public static void RemapXcodeExtension() {
AppDomain.CurrentDomain.AssemblyResolve -=
ResolveUnityEditoriOSXcodeExtension;
AppDomain.CurrentDomain.AssemblyResolve +=
ResolveUnityEditoriOSXcodeExtension;
}
/// <summary>
/// Reset settings of this plugin to default values.
/// </summary>
internal static void RestoreDefaultSettings() {
settings.DeleteKeys(PREFERENCE_KEYS);
analytics.RestoreDefaultSettings();
}
/// <summary>
/// The method used to integrate Cocoapods with the build.
/// </summary>
public enum CocoapodsIntegrationMethod {
None = 0,
Project,
Workspace
};
/// <summary>
/// When first upgrading, decide on workspace integration based on previous settings.
/// </summary>
private static int CocoapodsIntegrationUpgradeDefault {
get {
return LegacyCocoapodsInstallEnabled ?
(int)CocoapodsIntegrationMethod.Workspace :
(int)CocoapodsIntegrationMethod.Project;
}
}
/// <summary>
/// IOSResolver Unity Preferences setting indicating which CocoaPods integration method to use.
/// </summary>
public static CocoapodsIntegrationMethod CocoapodsIntegrationMethodPref {
get {
return (CocoapodsIntegrationMethod)settings.GetInt(
PREFERENCE_COCOAPODS_INTEGRATION_METHOD,
defaultValue: CocoapodsIntegrationUpgradeDefault);
}
set { settings.SetInt(PREFERENCE_COCOAPODS_INTEGRATION_METHOD, (int)value); }
}
/// <summary>
/// Deprecated: Enable / disable CocoaPods installation.
/// Please use CocoapodsIntegrationEnabled instead.
/// </summary>
[System.Obsolete("CocoapodsInstallEnabled is deprecated, please use " +
"CocoapodsIntegrationEnabled instead.")]
public static bool CocoapodsInstallEnabled {
get { return LegacyCocoapodsInstallEnabled; }
set { LegacyCocoapodsInstallEnabled = value; }
}
/// <summary>
/// A formerly used setting for project integration.
/// It's kept as a private function to seed the default for the new setting:
/// CocoapodsIntegrationEnabled.
/// </summary>
private static bool LegacyCocoapodsInstallEnabled {
get { return settings.GetBool(PREFERENCE_COCOAPODS_INSTALL_ENABLED,
defaultValue: true); }
set { settings.SetBool(PREFERENCE_COCOAPODS_INSTALL_ENABLED, value); }
}
/// <summary>
/// Enable / disable Podfile generation.
/// </summary>
public static bool PodfileGenerationEnabled {
get { return settings.GetBool(PREFERENCE_PODFILE_GENERATION_ENABLED,
defaultValue: true); }
set {
settings.SetBool(PREFERENCE_PODFILE_GENERATION_ENABLED, value);
SetEnablePollTargetSdks(value);
}
}
/// <summary>
/// Enable / disable polling of target iOS and tvOS SDK project setting values.
/// </summary>
private static void SetEnablePollTargetSdks(bool enable) {
if (enable && EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS) {
RunOnMainThread.OnUpdate += PollTargetIosSdk;
} else {
RunOnMainThread.OnUpdate -= PollTargetIosSdk;
}
if (enable && EditorUserBuildSettings.activeBuildTarget == BuildTarget.tvOS) {
RunOnMainThread.OnUpdate += PollTargetTvosSdk;
} else {
RunOnMainThread.OnUpdate -= PollTargetTvosSdk;
}
}
/// <summary>
/// Enable / disable execution of the pod tool via the shell.