-
Notifications
You must be signed in to change notification settings - Fork 434
Expand file tree
/
Copy pathIPhoneBuilder.java
More file actions
3548 lines (3162 loc) · 181 KB
/
IPhoneBuilder.java
File metadata and controls
3548 lines (3162 loc) · 181 KB
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 (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.builders;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author Shai Almog
* @author Steve Hannah
*/
public class IPhoneBuilder extends Executor {
private boolean useMetal;
private boolean enableGalleryMultiselect;
private boolean usePhotoKitForMultigallery;
private boolean enableWKWebView, disableUIWebView;
private String pod = "/usr/local/bin/pod";
private int podTimeout = 300000; // 5 minutes
private int xcodeVersion;
private static final String GOOGLE_SIGNIN_TUTORIAL_URL = "http://www.codenameone.com/...";
private File resultDir;
private boolean includePush;
private File tmpFile;
private File icon57;
private File icon512;
private static final String DEFAULT_MIN_DEPLOYMENT_VERSION = "12.0";
// StringBuilder used for constructing ruby script with xcodeproj
// which adds localized strings files to the project.
private StringBuilder installLocalizedStringsScript = new StringBuilder();
// Populated by processLocalizedIcons from cn1_icon_LANG[_COUNTRY].png files
// found in common/src/resources. Keys are alternate icon names embedded in the
// Info.plist (e.g. "AppIcon_en_GB"); values are the normalized locale match key
// (e.g. "en_GB" or just "en" when no country is supplied).
private Map<String, String> localizedIcons = new LinkedHashMap<String, String>();
private boolean detectJailbreak;
private boolean runPods=false;
private boolean runSpm=false;
private boolean photoLibraryUsage;
private String buildVersion;
private boolean usesLocalNotifications;
private boolean usesPurchaseAPI;
// so we need to store the main class name for later here.
// Map will be used for Xcode 8 privacy usage descriptions. Don't need it yet
// so leaving it commented out.
private Map<String,String> privacyUsageDescriptions = new HashMap<String,String>();
final static int majorOSVersion;
final static int minorOSVersion;
final static String osVersion;
static {
osVersion = System.getProperty("os.version");
StringTokenizer versionTok = new StringTokenizer(osVersion, ".");
majorOSVersion = Integer.parseInt(versionTok.nextToken());
minorOSVersion = Integer.parseInt(versionTok.nextToken());
}
public void cleanup() {
super.cleanup();
}
private static String maxVersionString(String commaDelimitedVersions) {
String[] versions = commaDelimitedVersions.split(",");
String currMax = "0.0";
for (String version : versions) {
version = version.trim();
if (version.length() == 0) {
continue;
}
if (compareVersionStrings(version, currMax) > 0) {
currMax = version;
}
}
return currMax;
}
private static int compareVersionStrings(String v1, String v2) {
String[] p1 = v1.split("\\.");
String[] p2 = v2.split("\\.");
int len = Math.max(p1.length, p2.length);
for (int i=0; i<len; i++) {
int iPart1 = p1.length > i ? Integer.parseInt(p1[i]) : 0;
int iPart2 = p2.length > i ? Integer.parseInt(p2[i]) : 0;
if (iPart1 != iPart2) {
return iPart1 < iPart2 ? -1 : 1;
}
}
return 0;
}
private void ensurePodsInstalled() throws BuildException {
if(!new File(pod).exists()) {
pod = "/usr/bin/pod";
if(!new File(pod).exists()) {
pod = "/opt/homebrew/bin/pod";
if(!new File(pod).exists()) {
log("You need to install cocoapods to proceed, to install cocoapods on your mac issue this command in the terminal: sudo gem install cocoapods --pre\n"
+ "followed by: sudo gem install xcodeproj");
throw new BuildException("Please install Cocoapods in order to use ios.dependencyManager=cocoapods or ios.pods");
}
}
}
try {
log("Pods version: " + execString(new File("."), pod, "--version"));
} catch (Exception ex) {
error("Please install Cocoapods in order to build iOS projects with CocoaPods. E.g. 'sudo gem install cocoapods'. See https://cocoapods.org/", ex);
throw new BuildException("Please install Cocoapods in order to build iOS projects with CocoaPods. E.g. 'sudo gem install cocoapods'. See https://cocoapods.org/");
}
}
private void ensureXcodeprojInstalled() throws BuildException {
try {
execString(new File("."), "ruby", "-e", "require 'xcodeproj'; puts Xcodeproj::VERSION");
} catch (Exception ex) {
throw new BuildException("Please install the xcodeproj Ruby gem to configure iOS Swift packages. E.g. 'sudo gem install xcodeproj'", ex);
}
}
private static String escapeRuby(String input) {
return input.replace("\\", "\\\\").replace("'", "\\'");
}
@Override
protected String getDeviceIdCode() {
return "\"\"";
}
/**
* Static libs that don't include the LC_VERSION_MIN_XXX run instructions seem
* to cause IPATool to crash. This occurs for .a archives compiled with Xcode before version 7.
* We should validate it here so that the error message is sensical. (It will fail in ipatool in the
* export step but the error won't make any sense..
* @param file
* @return
*/
private boolean validateLC_MIN_VERSION(File file) throws IOException {
ProcessBuilder pb = new ProcessBuilder("otool", "-lv", file.getAbsolutePath());
Process p = pb.start();
InputStream is = p.getInputStream();
Scanner scanner = new Scanner(is, "UTF-8");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if (line.contains("LC_VERSION_MIN_")) {
return true;
}
}
try {
p.waitFor();
} catch (InterruptedException ex) {
Logger.getLogger(IPhoneBuilder.class.getName()).log(Level.SEVERE, null, ex);
log(ex.getMessage());
}
return false;
}
private File getResDir() {
return new File(tmpFile, "res");
}
private File getBuildinRes() {
return new File(tmpFile, "btres");
}
private String minDeploymentTargets = "12.0";
private void addMinDeploymentTarget(String target) {
minDeploymentTargets += ","+target;
}
private String getDeploymentTarget(BuildRequest request){
StringBuilder sb = new StringBuilder();
sb.append(minDeploymentTargets);
if (request.getArg("ios.pods.platform", null) != null) {
sb.append(",");
sb.append(request.getArg("ios.pods.platform", ""));
}
if (request.getArg("ios.deployment_target", null) != null) {
sb.append(",");
sb.append(request.getArg("ios.deployment_target", ""));
}
if (request.getArg("ios.minDeploymentTarget", null) != null) {
sb.append(",");
sb.append(request.getArg("ios.minDeploymentTarget", ""));
}
return maxVersionString(sb.toString());
}
private static String append(String str, String separator, String append) {
if (!str.trim().endsWith(separator)) {
str += separator;
}
return str + append;
}
private int getDeploymentTargetInt(BuildRequest request) {
String target = getDeploymentTarget(request);
if (target.indexOf(".") > 0) {
target = target.substring(0, target.indexOf("."));
}
return Integer.parseInt(target);
}
@Override
public boolean build(File sourceZip, BuildRequest request) throws BuildException {
Stopwatch stopwatch = new Stopwatch();
addMinDeploymentTarget(DEFAULT_MIN_DEPLOYMENT_VERSION);
detectJailbreak = request.getArg("ios.detectJailbreak", "false").equals("true");
defaultEnvironment.put("LANG", "en_US.UTF-8");
tmpFile = tmpDir = getBuildDirectory();
useMetal = "true".equals(request.getArg("ios.metal", "false"));
log("Request Args: ");
log("-----------------");
for (String arg : request.getArgs()) {
log(arg+"="+request.getArg(arg, null));
}
log("-------------------");
buildVersion = request.getVersion();
if(request.getArg("ios.twoDigitVersion", "false").equals("true")) {
try {
float version = Float.parseFloat(buildVersion);
int intVersion = Math.round(100 * version);
int lsb = intVersion % 100;
buildVersion = "" + (intVersion / 100) + ".";
if(lsb == 0) {
buildVersion += "00";
} else {
if(lsb < 10) {
buildVersion += "0" + lsb;
} else {
buildVersion += lsb;
}
}
} catch(Exception err) {
}
}
for (String arg : request.getArgs()) {
if (arg.startsWith("ios.NS") && arg.endsWith("UsageDescription")) {
if (arg.toUpperCase().contains("PHOTOLIBRARY")) {
photoLibraryUsage = true;
}
privacyUsageDescriptions.put(arg.substring(arg.lastIndexOf(".")+1), request.getArg(arg, null));
}
}
String xcodebuild;
String iosPods = request.getArg("ios.pods", "");
enableGalleryMultiselect = "true".equals(request.getArg("ios.enableGalleryMultiselect", "false"));
if (enableGalleryMultiselect) {
if (!iosPods.contains("QBImagePickerController") && photoLibraryUsage) {
if (!iosPods.endsWith(",")) {
iosPods += ",";
}
iosPods += "QBImagePickerController ~> 3.4";
addMinDeploymentTarget("8.0");
}
}
usePhotoKitForMultigallery = "true".equals(request.getArg("ios.usePhotoKitForMultigallery", "false"));
enableWKWebView = "true".equals(request.getArg("ios.useWKWebView", "true"));
if (enableWKWebView) {
addMinDeploymentTarget("8.0");
}
disableUIWebView = enableWKWebView && "true".equals(request.getArg("ios.noUIWebView", "true"));
boolean bicodeHandle = true;
xcodebuild = resolveXcodebuild();
xcodeVersion = getXcodeVersion(xcodebuild);
if (xcodeVersion <= 0) {
xcodeVersion = 10;
}
String facebookAppId = request.getArg("facebook.appId", null);
boolean usePodsForFacebook = !request.getArg("ios.facebook.usePods", "true").equals("false") && facebookAppId != null && facebookAppId.length() > 0;
if (usePodsForFacebook) {
String fbPodsVersion = request.getArg("ios.facebook.version", "~>5.6.0");
addMinDeploymentTarget("10.0");
iosPods += (((iosPods.length() > 0) ? ",":"") + "FBSDKCoreKit "+fbPodsVersion+",FBSDKLoginKit "+fbPodsVersion+",FBSDKShareKit "+fbPodsVersion);
}
String googleAdUnitId = request.getArg("ios.googleAdUnitId", request.getArg("google.adUnitId", null));
boolean usePodsForGoogleAds = googleAdUnitId != null && googleAdUnitId.length() > 0;
if (usePodsForGoogleAds) {
iosPods += (((iosPods.length() > 0) ? ",":"") + "Firebase/Core,Firebase/AdMob");
addMinDeploymentTarget("7.0");
}
if (enableGalleryMultiselect && photoLibraryUsage) {
addMinDeploymentTarget("8.0");
}
if (enableWKWebView) {
addMinDeploymentTarget("8.0");
}
IOSDependencyConfig dependencyConfig = IOSDependencyManager.resolve(request, iosPods);
iosPods = dependencyConfig.iosPods;
runPods = dependencyConfig.usesCocoaPods();
runSpm = dependencyConfig.usesSwiftPackages();
if (runPods) {
ensurePodsInstalled();
}
if (runSpm) {
ensureXcodeprojInstalled();
}
debug("Xcode version is "+xcodeVersion);
// ios.themeMode stays the platform-specific knob; nativeTheme is
// the cross-platform meta hint. modern / legacy on the meta hint
// translate to the equivalent iOS values when ios.themeMode is unset.
// cn1.nativeTheme is honored as a deprecated alias for nativeTheme.
String iosMode = request.getArg("ios.themeMode", null);
if (iosMode == null) {
String sharedMode = request.getArg("nativeTheme",
request.getArg("cn1.nativeTheme", null));
if ("legacy".equalsIgnoreCase(sharedMode)) {
iosMode = "ios7";
} else if ("modern".equalsIgnoreCase(sharedMode)) {
iosMode = "modern";
} else {
iosMode = "auto";
}
}
tmpFile = getBuildDirectory();
if (tmpFile == null) {
throw new IllegalStateException("Build directory must be set before running build.");
}
if (tmpFile.exists()) {
delTree(tmpFile);
}
tmpFile.mkdirs();
File classesDir = new File(tmpFile, "classes");
classesDir.mkdirs();
File resDir = new File(tmpFile, "res");
resDir.mkdirs();
File buildinRes = new File(tmpFile, "btres");
buildinRes.mkdirs();
// fill classes dir from JAR and proper ports
try {
unzip(sourceZip, classesDir, resDir, resDir, buildinRes);
} catch (IOException ex) {
throw new BuildException("Failed to unzip source Zip file.", ex);
}
stopwatch.split("Setup & Unzip");
// We allow devs to add local podspecs inside a folder called "podspecs". This will
// be tarred by unzip() into a file named podspecs.tar so that folder hierarchies can be preserved
// We must now go through and extract this tar file into a separate directory so that we can copy them
// into the project folder after ByteCodeTranslator has created the Xcode project.
// Look for frameworks and localized strings
Set<String> variantGroups = new HashSet<String>();
for (File child : resDir.listFiles()) {
if (child.getName().endsWith(".lproj.zip")) {
// This is a zipped lproj directory that contains localized strings.
// We need to extract this and add the localized files to the project.
String languageBase = child.getName().substring(0, child.getName().lastIndexOf(".lproj.zip"));
File languageDir = new File(new File(tmpDir, "dist"), languageBase+".lproj");
if (languageDir.exists()) {
delTree(languageDir, true);
}
languageDir.mkdirs();
log("Found native strings directory "+child+". Attempting extract it and add it to the project");
try {
if (!exec(resDir, "unzip", child.getName(), "-d", languageDir.getAbsolutePath())) {
log("Failed to unzip " + child.getName());
return false;
}
} catch (Exception ex) {
throw new BuildException("Failed to extract bundled strings directory "+child, ex);
}
if (languageDir.exists()) {
// Sometmes files are zipped with the language dir as a directory within the root. We need to detect this
// case and fix it.
File nestedLanguageDir = new File(languageDir, languageDir.getName());
if (nestedLanguageDir.exists() && nestedLanguageDir.isDirectory()) {
for (File nestedStringsFile : nestedLanguageDir.listFiles()) {
if (!nestedStringsFile.getName().endsWith(".strings")) {
continue;
}
File destStringsFile = new File(languageDir, nestedStringsFile.getName());
try {
Files.copy(nestedStringsFile.toPath(), destStringsFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
log("Failed to reparent nested strings file "+nestedStringsFile+" to "+destStringsFile);
}
}
delTree(nestedLanguageDir, true);
}
}
if (!languageDir.exists()) {
log("Cannot find localization directory "+languageDir+" after extracting "+child+". Please ensure that the localization file is located in the top level of the zip file.");
return false;
}
// Create the Ruby xcodeproj script that will add the strings files to the project
for (File stringsFile : languageDir.listFiles()) {
if (!stringsFile.getName().endsWith(".strings")) {
// We only care about strings files.
continue;
}
if (installLocalizedStringsScript.length() == 0) {
installLocalizedStringsScript.append("variant_groups={}\n");
}
if (!variantGroups.contains(stringsFile.getName())) {
variantGroups.add(stringsFile.getName());
installLocalizedStringsScript.append("variant_group = xcproj.main_group.new_variant_group('").append(stringsFile.getName()).append("')\n");
installLocalizedStringsScript.append("variant_groups['").append(stringsFile.getName()).append("'] = variant_group\n");
//installLocalizedStringsScript.append("xcproj.targets.find{|e|e.name=='").append(request.getMainClass()).append("'}.add_file_reference(variant_group)\n");
}
installLocalizedStringsScript.append("fileref = variant_groups['").append(stringsFile.getName()).append("'].new_file('").append(languageDir.getName()).append("/").append(stringsFile.getName()).append("')\n");
installLocalizedStringsScript.append("xcproj.targets.each{|e| e.add_resources([fileref])}\n");
}
child.delete();
}
if (child.getName().endsWith(".framework.zip")) {
log("Found framework "+child+". Attempting extract it and generate podspec for it");
try {
if (!exec(resDir, "ditto", "-x", "-k", child.getAbsolutePath(), new File(tmpDir, "dist").getAbsolutePath())) {
log("Failed to unzip " + child.getName());
return false;
}
} catch (Exception ex) {
throw new BuildException("Failed to extract bundled framework "+child, ex);
}
String frameworkBase = child.getName().substring(0, child.getName().lastIndexOf(".framework.zip"));
File frameworkFile = new File(new File(tmpDir, "dist"), frameworkBase+".framework");
if (!frameworkFile.exists()) {
log("Cannot find framework file "+frameworkFile+" after extracting "+child+". Please ensure that the framework is located in the top level of the zip file.");
return false;
}
File podspecFile = new File(resDir, frameworkBase+".podspec");
StringBuilder podspecContents = new StringBuilder()
.append("Pod::Spec.new do |s|\n" +
" s.name = \""+frameworkBase+"\"\n" +
" s.version = \"1.0.0\"\n" +
" s.summary = \""+frameworkBase+" framework\"\n" +
" s.description = \"This spec specifies a vendored framework.\"\n" +
" s.platform = :ios\n" +
" s.homepage = \"https://www.codenameone.com\"\n" +
" s.source = {:path => \".\"}\n" +
" s.author = \"Codename One\"\n" +
" s.vendored_frameworks = \""+frameworkBase+".framework\"\n" +
"end");
log("Writing podspec "+podspecFile+" with contents:\n"+podspecContents.toString());
FileOutputStream fos = null;
try {
fos = new FileOutputStream(podspecFile);
fos.write(podspecContents.toString().getBytes("UTF-8"));
} catch (IOException ex) {
throw new BuildException("Failed to write th podxspec file for bundled framework "+child, ex);
} finally {
if (fos != null) {
try {fos.close();} catch (Throwable t){}
}
}
child.delete();
iosPods = append(iosPods, ",", frameworkBase);
}
}
stopwatch.split("Extract Extensions");
File podSpecs = new File(tmpFile, "podspecs");
podSpecs.mkdirs();
try {
for (File dir : new File[]{classesDir, resDir, buildinRes}) {
for (File child : dir.listFiles()) {
if (child.getName().endsWith(".podspec")) {
Files.move(child.toPath(), new File(podSpecs, child.getName()).toPath(), StandardCopyOption.REPLACE_EXISTING);
}
if ("podspecs.tar".equals(child.getName())) {
if (!exec(tmpFile, "tar", "xvf", child.getAbsolutePath(), "-C", podSpecs.getAbsolutePath())) {
log("Failed to extract podspecs tar file " + child.getAbsolutePath() + " to podspecs dir " + podSpecs.getAbsolutePath());
return false;
}
child.delete();
}
}
}
} catch (Exception ex) {
throw new BuildException("An error occurred while attempting to install bundled podspecs", ex);
}
File googleServicePlistFile = new File(resDir, "GoogleService-Info.plist");
String googleClientId = null;
boolean useGoogleSignIn = false;
if (googleServicePlistFile.exists()) {
googleServicePlist = new GoogleServicePlist();
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
Document doc;
try {
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
doc = dBuilder.parse(googleServicePlistFile);
} catch (Exception ex) {
throw new BuildException("Failed to parse google services Plist File", ex);
}
Element dict = (Element)doc.getElementsByTagName("dict").item(0);
NodeList childNodes = dict.getChildNodes();
int len = childNodes.getLength();
for (int i=0; i<len; i++) {
Node n = childNodes.item(i);
if (n instanceof Element) {
Element e = (Element)n;
if ("key".equals(e.getTagName()) && "CLIENT_ID".equals(e.getTextContent().trim())) {
Element nextEl = getNextElement(childNodes, i);
if (nextEl != null && "string".equals(nextEl.getTagName())) {
googleClientId = nextEl.getTextContent().trim();
googleServicePlist.clientId = googleClientId;
}
} else if ("key".equals(e.getTagName()) && "BUNDLE_ID".equals(e.getTextContent().trim())) {
Element nextEl = getNextElement(childNodes, i);
if (nextEl != null && "string".equals(nextEl.getTagName())) {
String bid = nextEl.getTextContent().trim();
if (bid == null || !bid.equals(request.getPackageName())) {
debug("Bundle ID="+request.getPackageName()+"; GoogleService BUNDLE_ID="+bid);
debug("GoogleService-Info.plist file bundle ID does not match the App ID. See "+GOOGLE_SIGNIN_TUTORIAL_URL+" for instructions on setting up GoogleSignIn");
log("GoogleService-Info.plist file bundle ID does not match the App ID. See "+GOOGLE_SIGNIN_TUTORIAL_URL+" for instructions on setting up GoogleSignIn");
return false;
}
}
} else if ("key".equals(e.getTagName()) && "IS_SIGNIN_ENABLED".equals(e.getTextContent().trim())) {
Element nextEl = getNextElement(childNodes, i);
if ("true".equals(nextEl.getTagName())) {
useGoogleSignIn = true;
googleServicePlist.useSignIn = true;
}
} else if ("key".equals(e.getTagName()) && "REVERSED_CLIENT_ID".equals(e.getTextContent().trim())) {
Element nextEl = getNextElement(childNodes, i);
if (nextEl != null && "string".equals(nextEl.getTagName())) {
//googleClientId = nextEl.getTextContent().trim();
googleServicePlist.reverseClientId = nextEl.getTextContent().trim();
}
}
}
}
}
stopwatch.split("Google Services Setup");
if (googleClientId == null && useGoogleSignIn) {
log("GoogleService-Info.plist file specifies that GoogleSignIn should be used but it doesn't provide a client ID. Likely the GoogleService-Info.plist file is not valid. See "+GOOGLE_SIGNIN_TUTORIAL_URL+" for instructions on setting up GoogleSignIn");
error("Fail 2", new RuntimeException("Need to provide GoogleService-Info.plist file"));
return false;
}
if (googleClientId == null) {
googleClientId = request.getArg("ios.gplus.clientId", null);
if (googleClientId != null) {
useGoogleSignIn = true;
}
}
if (useGoogleSignIn) {
iosPods += (((iosPods.length() > 0) ? ",":"") + "GoogleSignIn ~>5.0.0");
addMinDeploymentTarget("8.0");
}
try {
scanClassesForPermissions(classesDir, new Executor.ClassScanner() {
@Override
public void usesClass(String cls) {
if (cls == null) return;
if (!usesLocalNotifications && cls.indexOf("com/codename1/notifications/LocalNotification") == 0) {
usesLocalNotifications = true;
}
if (!usesPurchaseAPI && cls.indexOf("com/codename1/payment") == 0) {
usesPurchaseAPI = true;
}
}
@Override
public void usesClassMethod(String cls, String method) {
}
});
} catch (Exception ex) {
throw new BuildException("Failed to scan project classes for permissions.", ex);
}
stopwatch.split("Scan Classes");
debug("Local Notifications "+(usesLocalNotifications?"enabled":"disabled"));
try {
unzip(getResourceAsStream("/iOSPort.jar"), classesDir, buildinRes, buildinRes);
} catch (IOException ex) {
throw new BuildException("Failed to extract the iOSPort jar", ex);
}
// Check to make sure that static libraries include the LC_VERSION_MIN_XXX run commands
// so that ipatool doesn't choke.
// See https://stackoverflow.com/questions/47816371/getting-ios-development-build-error#
// And http://thomask.sdf.org/blog/2015/09/15/xcode-7s-new-linker-rules.html
boolean foundImproperStaticLibs = false;
try {
for (File f : buildinRes.listFiles()) {
if (f.getName().endsWith(".a")) {
if (!validateLC_MIN_VERSION(f)) {
log("WARNING: The static library " + f.getName() + " is missing the LC_MIN_VERSION_IPHONEOS run command which is required by the Xcode build tools. This generally means that it was compiled with an older version of Xcode which didn't include this command. Unfortunately, Xcode 7 now requires this command to be embedded into all static libraries. Please recompile this library with Xcode 7 or higher. If this library has been embedded as part of a cn1lib, you will need to update the cn1lib with the newly compiled static library. You may also want to look at changing the library to use Cocoapods instead of embedding the static lib directly.");
foundImproperStaticLibs = true;
}
}
}
} catch (Exception ex) {
throw new BuildException("Exception while trying to verify static libraries", ex);
}
if (foundImproperStaticLibs && "true".equals(request.getArg("ios.failOnWarning", "false"))) {
// For now, we'll make the default behaviour such that we don't automatically fail when a static
// lib doesn't have LC_VERSION_MIN because it is possible that compilation will still work. E.g.
// libzbar.a in the cn1-codescan library and little monkey QR reader doesn't include this
// and it doesn't seem to cause export to fail (need to test this).
log("Cancelling build due to static library warnings. Set ios.failOnWarning build hint to 'false' to ignore these warnings.");
return false;
}
try {
unzip(getResourceAsStream("/nativeios.jar"), classesDir, buildinRes, buildinRes);
} catch (IOException ex) {
throw new BuildException("Failed to extract nativeios.jar",ex);
}
stopwatch.split("Extract Libs");
if(request.getArg("noExtraResources", "false").equals("true")) {
new File(buildinRes, "CN1Resource.res").delete();
new File(buildinRes, "IPhoneTheme.res").delete();
new File(buildinRes, "iOS7Theme.res").delete();
}
if (useMetal) {
try {
File CN1ES2compat = new File(buildinRes, "CN1ES2compat.h");
replaceInFile(CN1ES2compat, "//#define CN1_USE_METAL", "#define CN1_USE_METAL");
copy(new File(buildinRes, "MainWindowMETAL.xib"), new File(buildinRes, "MainWindow.xib"));
copy(new File(buildinRes, "CodenameOne_METALViewController.xib"), new File(buildinRes, "CodenameOne_GLViewController.xib"));
} catch (Exception ex) {
throw new BuildException("Failed to inject Metal controllers", ex);
}
} else {
new File(buildinRes, "MainWindowMETAL.xib").delete();
new File(buildinRes, "CodenameOne_METALViewController.xib").delete();
}
final String moPubAdUnitId = request.getArg("ios.mopubId", null);
final String moPubTabletAdUnitId = request.getArg("ios.mopubTabletId", moPubAdUnitId);
if(moPubAdUnitId != null && moPubAdUnitId.length() > 0) {
try {
File CodenameOne_GLViewController = new File(buildinRes, "CodenameOne_GLViewController.h");
unzip(getResourceAsStream("/MoPubSDK_ios.zip"), classesDir, buildinRes, buildinRes);
replaceInFile(CodenameOne_GLViewController, "//#define INCLUDE_MOPUB", "#define INCLUDE_MOPUB");
replaceInFile(CodenameOne_GLViewController, "#define MOPUB_AD_UNIT", "#define MOPUB_AD_UNIT @\"" + moPubAdUnitId + "\"");
replaceInFile(CodenameOne_GLViewController, "#define MOPUB_AD_SIZE", "#define MOPUB_AD_SIZE " + request.getArg("ios.mopubAdSize", "MOPUB_BANNER_SIZE"));
replaceInFile(CodenameOne_GLViewController, "#define MOPUB_TABLET_AD_UNIT", "#define MOPUB_TABLET_AD_UNIT @\"" + moPubTabletAdUnitId + "\"");
replaceInFile(CodenameOne_GLViewController, "#define MOPUB_TABLET_AD_SIZE", "#define MOPUB_TABLET_AD_SIZE " + request.getArg("ios.mopubTabletAdSize", "MOPUB_LEADERBOARD_SIZE"));
} catch (Exception ex) {
throw new BuildException("Failed to inject MoPubSDK");
}
}
String microphoneCallback = "";
if(request.getArg("ios.headphoneCallback", "false").equals("true")) {
try {
File headphoneDetectorM = new File(buildinRes, "HeadphonesDetector.m");
File headphoneDetectorH = new File(buildinRes, "HeadphonesDetector.h");
replaceInFile(headphoneDetectorM, "//#define DETECT_HEADPHONE", "#define DETECT_HEADPHONE");
replaceInFile(headphoneDetectorH, "//#define DETECT_HEADPHONE2", "#define DETECT_HEADPHONE2");
microphoneCallback =
" public void headphonesDisconnected() {\n"
+ " i.headphonesDisconnected();\n"
+ " }\n\n"
+ " public void headphonesConnected() {\n"
+ " i.headphonesConnected();\n"
+ " }\n\n";
} catch (Exception ex) {
throw new BuildException("Failed to add microphone callbacks", ex);
}
}
File launchStoryboard = new File(buildinRes, "LaunchScreen-Default.storyboard");
if (xcodeVersion < 9) {
launchStoryboard.delete();
}
File glAppDelegate = new File(buildinRes, "CodenameOne_GLAppDelegate.m");
boolean useUIScene = "true".equalsIgnoreCase(request.getArg("ios.uiscene", "true"));
String integrateFacebook = "";
if(facebookAppId != null && facebookAppId.length() > 0) {
try {
if (usePodsForFacebook) {
} else {
String facebookFile = "/facebook-ios-sdk-4.12.zip";
unzip(getResourceAsStream(facebookFile), classesDir, buildinRes, buildinRes);
}
integrateFacebook = " com.codename1.social.FacebookImpl.init(com.codename1.impl.ios.IOSImplementation.nativeInstance);\n"
+ " Display.getInstance().setProperty(\"facebook_app_id\", \"" + facebookAppId + "\");\n";
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define INCLUDE_FACEBOOK_CONNECT", "#define INCLUDE_FACEBOOK_CONNECT");
if (usePodsForFacebook) {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define USE_FACEBOOK_CONNECT_PODS", "#define USE_FACEBOOK_CONNECT_PODS");
}
String defaultPermissions = "\"public_profile\", \"email\", \"user_friends\"";
String permissions = request.getArg("ios.facebook_permissions", request.getArg("and.facebook_permissions", defaultPermissions));
StringTokenizer t = new StringTokenizer(permissions, " ,\n\r\t");
permissions = "";
permissions += "@" + t.nextToken();
while (t.hasMoreTokens()) {
permissions += ", @" + t.nextToken();
}
replaceInFile(new File(buildinRes, "FacebookImpl.m"), "@\"basic_info\"", permissions);
} catch (Exception ex) {
throw new BuildException("Failed to add facebook api", ex);
}
}
String integrateGoogleConnect = "";
if (useGoogleSignIn) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define INCLUDE_GOOGLE_CONNECT", "#define INCLUDE_GOOGLE_CONNECT");
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define GOOGLE_SIGNIN", "#define GOOGLE_SIGNIN");
integrateGoogleConnect = " com.codename1.social.GoogleImpl.init(com.codename1.impl.ios.IOSImplementation.nativeInstance);\n"
+ " Display.getInstance().setProperty(\"ios.gplus.clientId\", \"" + googleClientId + "\");\n";
} catch (IOException ex) {
throw new BuildException("Failed to inject google signin support", ex);
}
}
boolean enableBackgroundFetch = request.getArg("ios.background_modes", "").contains("fetch");
if (enableBackgroundFetch) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define INCLUDE_CN1_BACKGROUND_FETCH", "#define INCLUDE_CN1_BACKGROUND_FETCH");
} catch (IOException ex) {
throw new BuildException("Failed to add background fetch support", ex);
}
}
if(request.getArg("ios.usePrintf","false").equals("true")) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "#define CN1Log(str,...) NSLog(str,##__VA_ARGS__)", "#define CN1Log(str,...) printf([[NSString stringWithFormat:str,##__VA_ARGS__] UTF8String])");
} catch (IOException ex) {
throw new BuildException("Failed to process ios.usePrintf build hint");
}
}
boolean disableSignalHandler = request.getArg("ios.convertSignalsToExceptions", "true").equals("false");
if (disableSignalHandler) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLAppDelegate.m"), "installSignalHandlers();", "//installSignalHandlers();");
} catch (IOException ex) {
throw new BuildException("Failed to process ios.convertSignalsToExceptions build hint", ex);
}
}
boolean enableBackgroundLocation = request.getArg("ios.background_modes", "").contains("location");
if (enableBackgroundLocation) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define CN1_ENABLE_BACKGROUND_LOCATION", "#define CN1_ENABLE_BACKGROUND_LOCATION");
} catch (IOException ex) {
throw new BuildException("Failed to process ios.background_modes location build hint", ex);
}
}
if (enableGalleryMultiselect) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define ENABLE_GALLERY_MULTISELECT", "#define ENABLE_GALLERY_MULTISELECT");
} catch (IOException ex) {
throw new BuildException("Failed to enabled gallery multiselect support", ex);
}
}
if (usePhotoKitForMultigallery) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define USE_PHOTOKIT_FOR_MULTIGALLERY", "#define USE_PHOTOKIT_FOR_MULTIGALLERY");
} catch (IOException ex) {
throw new BuildException("Failed to enabled gallery multiselect support", ex);
}
}
if (enableWKWebView) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define ENABLE_WKWEBVIEW", "#define ENABLE_WKWEBVIEW");
} catch (IOException ex) {
throw new BuildException("Failure while enabing WKWebView support", ex);
}
}
if (disableUIWebView) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define NO_UIWEBVIEW", "#define NO_UIWEBVIEW");
} catch (IOException ex) {
throw new BuildException("Failure while disabling UIWebView support", ex);
}
}
if (xcodeVersion >= 9) {
try {
for (String privacyKey : privacyUsageDescriptions.keySet()) {
String defKey = "INCLUDE_" + privacyKey.replace("UsageDescription", "_USAGE").substring(2).toUpperCase();
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define " + defKey, "#define " + defKey);
}
if (request.getArg("ios.locationUsageDescription", null) != null) {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define INCLUDE_LOCATION_USAGE", "#define INCLUDE_LOCATION_USAGE");
}
} catch (IOException ex) {
throw new BuildException("Failed to add privacy usage descriptions", ex);
}
} else {
photoLibraryUsage = true;
String[] defines = {"INCLUDE_CONTACTS_USAGE", "INCLUDE_CALENDARS_USAGE", "INCLUDE_CAMERA_USAGE",
"INCLUDE_FACEID_USAGE", "INCLUDE_LOCATION_USAGE", "INCLUDE_MICROPHONE_USAGE", "INCLUDE_MOTION_USAGE",
"INCLUDE_PHOTOLIBRARYADD_USAGE", "INCLUDE_PHOTOLIBRARY_USAGE", "INCLUDE_REMINDERS_USAGE",
"INCLUDE_SIRI_USAGE", "INCLUDE_SPEECHRECOGNITION_USAGE", "INCLUDE_NFCREADER_USAGE"
};
try {
for (String defKey : defines) {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define " + defKey, "#define " + defKey);
}
} catch (IOException ex) {
throw new BuildException("Failed to process usage descriptions", ex);
}
}
if ("true".equals(request.getArg("ios.blockScreenshotsOnEnterBackground", "false"))) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLViewController.h"), "//#define CN1_BLOCK_SCREENSHOTS_ON_ENTER_BACKGROUND", "#define CN1_BLOCK_SCREENSHOTS_ON_ENTER_BACKGROUND");
} catch (IOException ex) {
throw new BuildException("Failure while processing ios.blockScreenshotsOnEnterBackground build hint", ex);
}
}
if (useUIScene) {
try {
replaceInFile(new File(buildinRes, "CodenameOne_GLAppDelegate.h"), "#ifdef CN1_USE_UI_SCENE", "#define CN1_USE_UI_SCENE\n#ifdef CN1_USE_UI_SCENE");
} catch (IOException ex) {
throw new BuildException("Failure while processing ios.uiscene build hint", ex);
}
}
String applicationDidEnterBackground = request.getArg("ios.applicationDidEnterBackground", null);
if(applicationDidEnterBackground != null) {
try {
replaceInFile(glAppDelegate, "//----application_will_resign_active", applicationDidEnterBackground);
} catch (IOException ex) {
throw new BuildException("Failure while processing ios.applicationDidEnterBackground build hint", ex);
}
}
try {
if (request.getArg("ios.lowMemCamera", "false").equals("true")) {
File CodenameOne_GLViewController = new File(buildinRes, "CodenameOne_GLViewController.m");
replaceInFile(CodenameOne_GLViewController, "//#define LOW_MEM_CAMERA", "#define LOW_MEM_CAMERA");
}
if (request.getArg("ios.enableStatusBar7", "true").equals("false")) {
File CodenameOne_GLViewController = new File(buildinRes, "CodenameOne_GLViewController.m");
replaceInFile(CodenameOne_GLViewController, "int statusbarHeight = 20;", "int statusbarHeight = 0;");
}
if (request.getArg("ios.enableAutoplayVideo", "false").equals("false")) {
File iosNative = new File(buildinRes, "IOSNative.m");
replaceInFile(iosNative, "#define AUTO_PLAY_VIDEO", "//#define AUTO_PLAY_VIDEO");
}
if (request.getArg("ios.background_modes", "").contains("fetch")) {
replaceInFile(new File(buildinRes, "CodenameOne_GLAppDelegate.m"), "//#define INCLUDE_CN1_BACKGROUND_FETCH", "#define INCLUDE_CN1_BACKGROUND_FETCH");
}
} catch (IOException ex) {
throw new BuildException("Failure while trying to inject build hints into sources.", ex);
}
String viewDidLoad = request.getArg("ios.viewDidLoad", null);
String adPadding = request.getArg("ios.googleAdUnitIdPadding", "");
if(googleAdUnitId != null && googleAdUnitId.length() > 0) {
if(adPadding.length() == 0) {
adPadding = " Display.getInstance().setProperty(\"adPaddingBottom\", \"9\");\n";
} else {
adPadding = " Display.getInstance().setProperty(\"adPaddingBottom\", \"" + adPadding + "\");\n";
}
}
stopwatch.split("Inject Build Hints");
File stubSource = new File(tmpFile, "stub");
stubSource.mkdirs();
try {
generateUnitTestFiles(request, stubSource);
} catch (Exception ex) {
throw new BuildException("Failed to generate Unit Test Files", ex);
}
stopwatch.split("Generate Unit Tests");
String newStorage = "";
if(request.getArg("ios.newStorageLocation", "true").equals("true")) {
newStorage = " Display.getInstance().setProperty(\"iosNewStorage\", \"true\");\n";
}
String disableScreenshots = "";
if (request.getArg("ios.disableScreenshots", "false").equalsIgnoreCase("true")) {
disableScreenshots = " Display.getInstance().setProperty(\"DisableScreenshots\", \"true\");\n";
}
String didEnterBackground = " stopped = true;\n"
+ " final long bgTask = com.codename1.impl.ios.IOSImplementation.beginBackgroundTask();\n"
+ " Display.getInstance().callSerially(new Runnable() { \n"
+ " public void run(){ \n"
+ " i.stop();\n"
+ " com.codename1.impl.ios.IOSImplementation.endBackgroundTask(bgTask);"
+ " }\n"