-
-
Notifications
You must be signed in to change notification settings - Fork 136
/
erase-install.sh
executable file
·3560 lines (3231 loc) · 170 KB
/
erase-install.sh
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
#!/bin/zsh --no-rcs
# shellcheck shell=bash
# shellcheck disable=SC2001
# this is to use sed in the case statements
# shellcheck disable=SC2034,SC2296
# these are due to the dynamic variable assignments used in the localization strings
: <<DOC
==============================================================================
erase-install.sh
==============================================================================
by Graham Pugh
WARNING. This is a self-destruct script. Do not try it out on your own device!
See README.md and the GitHub repo's Wiki for details on use.
It is recommended to use the package installer of this script. It contains
swiftDialog and mist, which are required for most of the use-cases of this script.
This script can, however, also be run standalone.
It will download and install swiftDialog if needed and not found.
It will also download mist if it is not found.
Suppress the downloads with the --no-curl option.
Requirements:
- macOS 11+ (for use on older versions of macOS, download version 27.3 of erase-install.sh)
- Device file system is APFS
DOC
# =============================================================================
# Variables
# =============================================================================
# script name
script_name="erase-install"
pkg_label="com.github.grahampugh.erase-install"
# Version of this script
version="37.0"
# Directory in which to place the macOS installer. Overridden with --path
installer_directory="/Applications"
# Default working directory (may be overridden by the --workdir parameter)
workdir="/Library/Management/erase-install"
# Default logdir
logdir="/Library/Management/erase-install/log"
# mist tool
mist_bin="/usr/local/bin/mist"
# Required mist-cli version
# This ensures a compatible mist version is used if not using the package installer
mist_tag_required="v2.1.1"
# Required swiftDialog version
# This ensures a compatible swiftDialog version is used if not using the package installer
swiftdialog_tag_required="v2.5.4"
# Required swiftDialog version for macOS 11
# This ensures a compatible swiftDialog version is used if not using the package installer
swiftdialog_bigsur_tag_required="v2.2.1"
# swiftDialog variables
dialog_portable_app="$workdir/Dialog.app"
dialog_default_app="/Library/Application Support/Dialog/Dialog.app"
dialog_log=$(/usr/bin/mktemp /var/tmp/dialog.XXX)
dialog_output="/var/tmp/dialog.json"
# swiftDialog icons
dialog_dl_icon="/System/Library/PrivateFrameworks/SoftwareUpdate.framework/Versions/A/Resources/SoftwareUpdate.icns"
dialog_confirmation_icon="/System/Applications/System Settings.app"
dialog_warning_icon="SF=xmark.circle,colour=red"
dialog_fmm_icon="/System/Library/PrivateFrameworks/AOSUI.framework/Versions/A/Resources/findmy.icns"
dialog_icon_size="128"
# default app and package names for mist
default_downloaded_app_name="Install %NAME%.app"
default_downloaded_pkg_name="InstallAssistant-%VERSION%-%BUILD%.pkg"
default_downloaded_pkg_id="com.apple.InstallAssistant.%VERSION%.%BUILD%.pkg"
# =============================================================================
# Functions
# functions are listed alphabetically
# =============================================================================
# -----------------------------------------------------------------------------
# Open a dialog window to ask for the user's username and password.
# This is required on Apple Silicon Mac
# -----------------------------------------------------------------------------
ask_for_credentials() {
# set the dialog command arguments
get_default_dialog_args "utility"
dialog_args=("${default_dialog_args[@]}")
dialog_args+=(
"--title"
"${dialog_window_title}"
"--icon"
"${dialog_confirmation_icon}"
"--overlayicon"
"SF=key.fill"
"--iconsize"
"${dialog_icon_size}"
"--textfield"
"Username,prompt=$current_user"
"--textfield"
"Password,secure"
"--button1text"
"Continue"
"--timer"
"300"
"--hidetimerbar"
)
if [[ "$erase" == "yes" ]]; then
dialog_args+=(
"--message"
"${(P)dialog_erase_credentials}"
)
else
dialog_args+=(
"--message"
"${(P)dialog_reinstall_credentials}"
)
fi
if [[ $max_password_attempts != "infinite" ]]; then
dialog_args+=("-2")
fi
# run the dialog command
"$dialog_bin" "${dialog_args[@]}" 2>/dev/null > "$dialog_output"
}
# -----------------------------------------------------------------------------
# Dialogue to disable Find My Mac.
# Called when --check-fmm option is used.
# Not used in --silent mode.
# -----------------------------------------------------------------------------
check_fmm() {
# default Find My wait timer to 60 seconds
if [[ ! $fmm_wait_timer ]]; then
fmm_wait_timer=300
fi
if ! nvram -xp | grep fmm-mobileme-token-FMM > /dev/null ; then
writelog "[check_fmm] OK - Find My not enabled"
elif [[ $silent ]]; then
writelog "[check_fmm] ERROR - Find My enabled, cannot continue."
echo
exit 1
else
writelog "[check_fmm] WARNING - Find My enabled"
# set the dialog command arguments
get_default_dialog_args "utility"
dialog_args=("${default_dialog_args[@]}")
# original icon: ${dialog_confirmation_icon}
dialog_args+=(
"--title"
"${(P)dialog_fmm_title}"
"--icon"
"${dialog_confirmation_icon}"
"--overlayicon"
"${dialog_fmm_icon}"
"--iconsize"
"${dialog_icon_size}"
"--message"
"${(P)dialog_fmm_desc}"
"--timer"
"${fmm_wait_timer}"
)
# run the dialog command
"$dialog_bin" "${dialog_args[@]}" 2>/dev/null & sleep 0.1
# now count down while checking if Find My has been disabled
while [[ "$fmm_wait_timer" -gt 0 ]]; do
if ! nvram -xp | grep fmm-mobileme-token-FMM > /dev/null ; then
writelog "[check_fmm] OK - Find My not enabled"
# quit dialog
writelog "[check_fmm] Sending quit message to dialog log ($dialog_log)"
echo "quit:" >> "$dialog_log"
return
fi
sleep 1
((fmm_wait_timer--))
done
# quit dialog
writelog "[check_fmm] Sending quit message to dialog log ($dialog_log)"
echo "quit:" >> "$dialog_log"
# set the dialog command arguments
get_default_dialog_args "utility"
dialog_args=("${default_dialog_args[@]}")
dialog_args+=(
"--title"
"${(P)dialog_fmm_title}"
"--icon"
"${dialog_confirmation_icon}"
"--iconsize"
"${dialog_icon_size}"
"--overlayicon"
"${dialog_fmm_icon}"
"--message"
"${(P)dialog_fmmenabled_desc}"
)
# run the dialog command
"$dialog_bin" "${dialog_args[@]}" 2>/dev/null
writelog "[check_fmm] ERROR - Find My still enabled after waiting for ${fmm_wait_timer}s, cannot continue."
echo
exit 1
fi
}
# -----------------------------------------------------------------------------
# Checks for active meetings.
# Function taken from installomator, function hasDisplaySleepAssertion
# Called when --check-activty option is used.
# -----------------------------------------------------------------------------
check_for_presentation_activity() {
# Get the names of all apps with active display sleep assertions
local apps
apps="$(/usr/bin/pmset -g assertions | /usr/bin/awk '/NoDisplaySleepAssertion | PreventUserIdleDisplaySleep/ && match($0,/\(.+\)/) && ! /coreaudiod/ {gsub(/^.*\(/,"",$0); gsub(/\).*$/,"",$0); print};')"
if [[ ! "$apps" ]]; then
# No display sleep assertions detected
writelog "[check_for_presentation_activity] No active meetings detected. Continuing."
return
fi
# Create an array of apps that need to be ignored
IGNORE_DND_APPS="caffeinate"
local ignore_array=("${(@s/,/)IGNORE_DND_APPS}")
for app in ${(f)apps}; do
if (( ! ${ignore_array[(Ie)${app}]} )); then
# Relevant app with display sleep assertion detected
writelog "[check_for_presentation_activity] Active meeting detected (${app}). Exiting."
exit 0
fi
done
}
# -----------------------------------------------------------------------------
# Download mist if not present and not --silent mode
# -----------------------------------------------------------------------------
check_for_mist() {
if [[ -f "$mist_bin" ]]; then
# check mist version because older versions may not obtain a valid installer
mist_version=$("$mist_bin" --version | head -n 1 | cut -d' ' -f1)
if [[ v"$mist_version" == "$mist_tag_required" ]]; then
writelog "[check_for_mist] mist-cli $mist_tag_required is installed ($mist_bin)"
mist_is_compatible=1
else
writelog "[check_for_mist] mist-cli v$mist_version is installed ($mist_bin) - does not match required version $mist_tag_required"
mist_is_compatible=0
fi
else
writelog "[check_for_mist] mist-cli is not installed"
mist_is_compatible=0
fi
if [[ $mist_is_compatible -ne 1 ]]; then
if [[ ! $no_curl ]]; then
writelog "[check_for_mist] Downloading mist-cli..."
# obtain the download URL
mist_api_url="https://api.github.com/repos/ninxsoft/mist-cli/releases"
mist_download_url=$(/usr/bin/curl -sL -H "Accept: application/json" "$mist_api_url/tags/$mist_tag_required" | awk -F '"' '/browser_download_url/ { print $4; exit }')
if /usr/bin/curl -L "$mist_download_url" -o "$workdir/mist-cli.pkg" ; then
if installer -pkg "$workdir/mist-cli.pkg" -target / ; then
mist_is_compatible=1
else
writelog "[check_for_mist] WARNING! mist-cli installation failed"
fi
fi
fi
# check it did actually get downloaded
if [[ $mist_is_compatible -eq 1 ]]; then
writelog "[check_for_mist] mist-cli $mist_tag_required is installed ($mist_bin)"
elif [[ -f "$mist_bin" ]]; then
writelog "[check_for_mist] WARNING! mist-cli v$mist_version is installed ($mist_bin) - does not match required version $mist_tag_required"
else
writelog "[check_for_mist] ERROR! Could not download mist-cli. Cannot continue."
exit 1
fi
fi
}
# -----------------------------------------------------------------------------
# Download dialog if not present and not --silent mode
# -----------------------------------------------------------------------------
check_for_swiftdialog_app() {
# swiftDialog 2.3 and higher are incompatible with macOS 11. Remove this version if present.
if [[ -d "$dialog_portable_app" ]]; then
dialog_bin="$dialog_portable_app/Contents/MacOS/Dialog"
dialog_string=$("$dialog_bin" --version)
dialog_minor_vers=$(cut -d. -f1,2 <<< "$dialog_string")
if [[ $(echo "$dialog_minor_vers > 2.2" | bc) -eq 1 ]] && ! is-at-least "12" "$system_version"; then
writelog "[check_for_swiftdialog_app] swiftDialog v$dialog_string is installed but is not compatible with macOS $system_version. Removing v$dialog_string..."
/bin/rm -rf "$dialog_portable_app"
/bin/rm -f /var/tmp/dialog.*
dialog_bin=""
fi
fi
# check also for a pre-installed version
if [[ ! -d "$dialog_portable_app" && -d "$dialog_default_app" ]]; then
dialog_bin="$dialog_default_app/Contents/MacOS/Dialog"
dialog_string=$("$dialog_bin" --version)
dialog_minor_vers=$(cut -d. -f1,2 <<< "$dialog_string")
if [[ $(echo "$dialog_minor_vers > 2.2" | bc) -eq 1 ]] && ! is-at-least "12" "$system_version"; then
writelog "[check_for_swiftdialog_app] Preinstalled version of swiftDialog v$dialog_string is installed but is not compatible with macOS $system_version."
dialog_bin=""
fi
fi
# now check for any version of swiftDialog and download if not present
if [[ -f "$dialog_bin" && "v$dialog_string" == "$swiftdialog_tag_required"* ]]; then
writelog "[check_for_swiftdialog_app] swiftDialog binary v$dialog_string is installed ($dialog_bin)"
else
writelog "[check_for_swiftdialog_app] swiftDialog v$dialog_string is installed but the recommended version is $swiftdialog_tag_required."
if [[ ! $no_curl ]]; then
if ! is-at-least "12" "$system_version"; then
# we need to get the older version of swiftDialog that is compatible with Big Sur
swiftdialog_tag_required="$swiftdialog_bigsur_tag_required"
writelog "[check_for_swiftdialog_app] Downloading swiftDialog for macOS $system_version..."
# obtain the download URL
swiftdialog_api_url="https://api.github.com/repos/swiftDialog/swiftDialog/releases"
dialog_download_url=$(/usr/bin/curl -sL -H "Accept: application/json" "$swiftdialog_api_url/tags/$swiftdialog_tag_required" | ljt assets.0.browser_download_url -)
if /usr/bin/curl -L "$dialog_download_url" -o "$workdir/dialog.pkg" ; then
if installer -tgt / -pkg "$workdir/dialog.pkg" ; then
dialog_bin="$dialog_default_app/Contents/MacOS/Dialog"
dialog_string=$("$dialog_bin" --version)
dialog_minor_vers=$(cut -d. -f1,2 <<< "$dialog_string")
writelog "[check_for_swiftdialog_app] swiftDialog installation succeeded"
else
writelog "[check_for_swiftdialog_app] swiftDialog installation failed"
fi
else
writelog "[check_for_swiftdialog_app] ERROR: swiftDialog download failed"
exit 1
fi
else
writelog "[check_for_swiftdialog_app] Downloading swiftDialog..."
# obtain the download URL
swiftdialog_api_url="https://api.github.com/repos/swiftDialog/swiftDialog/releases"
dialog_download_url=$(/usr/bin/curl -sL -H "Accept: application/json" "$swiftdialog_api_url/tags/$swiftdialog_tag_required" | ljt assets.1.browser_download_url -)
if /usr/bin/curl -L "$dialog_download_url" -o "$workdir/swiftDialog.dmg" ; then
mount_point=$(/usr/bin/mktemp -d /Users/Shared/swiftDialog.XXX)
mkdir -p "$mount_point"
if /usr/bin/hdiutil attach -quiet -noverify -nobrowse "$workdir/swiftDialog.dmg" -mountpoint "$mount_point" ; then
writelog "[check_for_swiftdialog_app] Mounting $workdir/swiftDialog.dmg"
rm -Rf "$workdir/Dialog.app"
if cp -R "$mount_point/Dialog.app" "$workdir"/; then
dialog_bin="$dialog_portable_app/Contents/MacOS/Dialog"
dialog_string=$("$dialog_bin" --version)
dialog_minor_vers=$(cut -d. -f1,2 <<< "$dialog_string")
writelog "[check_for_swiftdialog_app] swiftDialog installation succeeded"
else
writelog "[check_for_swiftdialog_app] swiftDialog installation failed"
fi
diskutil unmount force "$mount_point"
rm -rf "$mount_point"
else
writelog "[check_for_swiftdialog_app] ERROR: could not mount swiftDialog disk image"
fi
rm "$workdir/swiftDialog.dmg"
else
writelog "[check_for_swiftdialog_app] ERROR: swiftDialog download failed"
fi
fi
fi
# check it did actually get downloaded
# writelog "[check_for_swiftdialog_app] swiftDialog v$dialog_string is installed" # TEMP
if [[ -f "$dialog_bin" ]]; then
writelog "[check_for_swiftdialog_app] swiftDialog v$dialog_string is installed ($dialog_bin)"
else
writelog "[check_for_swiftdialog_app] ERROR: Could not download swiftDialog."
exit 1
fi
fi
# ensure log file is writable
writelog "[check_for_swiftdialog_app] Creating dialog log ($dialog_log)..."
/usr/bin/touch "$dialog_log"
/usr/sbin/chown "${current_user}:wheel" "$dialog_log"
/bin/chmod 666 "$dialog_log"
}
# -----------------------------------------------------------------------------
# Determine if the amount of free and purgable drive space is sufficient for
# the upgrade to take place.
# The JavaScript osascript is used to give us the purgeable space as this is
# not available via any shell commands (Thanks to Pico).
# However, this does not work at the login window, so then we have to fall
# back to using df -h, which will not include purgeable space.
# -----------------------------------------------------------------------------
check_free_space() {
free_disk_space=$(osascript -l 'JavaScript' -e "ObjC.import('Foundation'); var freeSpaceBytesRef=Ref(); $.NSURL.fileURLWithPath('/').getResourceValueForKeyError(freeSpaceBytesRef, 'NSURLVolumeAvailableCapacityForImportantUsageKey', null); Math.round(ObjC.unwrap(freeSpaceBytesRef[0]) / 1000000000)")
if [[ ! "$free_disk_space" ]] || [[ "$free_disk_space" == 0 ]]; then
# fall back to df if the above fails
free_disk_space=$(df -Pk . | column -t | sed 1d | awk '{print $4}' | xargs -I{} expr {} / 1000000)
fi
# if there isn't enough space, then we show a failure message to the user
if [[ $free_disk_space -ge $min_drive_space ]]; then
writelog "[check_free_space] OK - $free_disk_space GB free/purgeable disk space detected"
elif [[ $silent ]]; then
writelog "[check_free_space] ERROR - $free_disk_space GB free/purgeable disk space detected"
echo
exit 1
else
writelog "[check_free_space] ERROR - $free_disk_space GB free/purgeable disk space detected"
# set the dialog command arguments
get_default_dialog_args "utility"
dialog_args=("${default_dialog_args[@]}")
dialog_args+=(
"--title"
"${dialog_window_title}"
"--icon"
"${dialog_confirmation_icon}"
"--iconsize"
"${dialog_icon_size}"
"--overlayicon"
"SF=externaldrive.fill.badge.xmark,colour=red"
"--message"
"${(P)dialog_check_desc}"
"--button1text"
"${(P)dialog_cancel_button}"
)
# run the dialog command
"$dialog_bin" "${dialog_args[@]}" 2>/dev/null
exit 1
fi
}
# -----------------------------------------------------------------------------
# Check the installer validity.
# The Build number in the app Info.plist is often older than the advertised
# build number, so it's not a great check for checking the validity of the installer
# if we are running --erase, where we might want to be using the same build.
# Since macOS 11, the actual build number is found in the SharedSupport.dmg in
# com_apple_MobileAsset_MacSoftwareUpdate.xml.
# For older OSs we include a fallback to the older, less accurate
# Info.plist file.
# -----------------------------------------------------------------------------
check_installer_is_valid() {
writelog "[check_installer_is_valid] Checking validity of $cached_installer_app."
# first ensure that an installer is not still mounted from a previous run as it might
# interfere with the check
[[ -d "/Volumes/Shared Support" ]] && diskutil unmount force "/Volumes/Shared Support"
# now attempt to mount the installer and grab the build number from
# com_apple_MobileAsset_MacSoftwareUpdate.xml
if [[ -f "$cached_installer_app/Contents/SharedSupport/SharedSupport.dmg" ]]; then
if /usr/bin/hdiutil attach -quiet -noverify -nobrowse "$cached_installer_app/Contents/SharedSupport/SharedSupport.dmg" ; then
writelog "[check_installer_is_valid] Mounting $cached_installer_app/Contents/SharedSupport/SharedSupport.dmg"
sleep 1
build_xml="/Volumes/Shared Support/com_apple_MobileAsset_MacSoftwareUpdate/com_apple_MobileAsset_MacSoftwareUpdate.xml"
if [[ -f "$build_xml" ]]; then
writelog "[check_installer_is_valid] Using Build value from com_apple_MobileAsset_MacSoftwareUpdate.xml"
installer_build=$(/usr/libexec/PlistBuddy -c "Print :Assets:0:Build" "$build_xml")
# Also get the compatible device/board IDs of the installer and compare with the system device/board ID
# 1. Grab device/board ID
get_device_id
# 2. Grab compatible device/board IDs from com_apple_MobileAsset_MacSoftwareUpdate
compatible_device_ids=$(grep -A2 "SupportedDeviceModels" "$build_xml" | grep string | awk -F '<string>|</string>' '{ print $2 }')
# 3. Check that 1 is in 2.
if [[ ($device_id && "$compatible_device_ids" == *"$device_id"*) || ($board_id && "$compatible_device_ids" == *"$board_id"*) ]]; then
writelog "[check_installer_is_valid] Installer is compatible with system"
else
writelog "[check_installer_is_valid] ERROR: Installer is incompatible with system"
invalid_installer_found="yes"
fi
else
writelog "[check_installer_is_valid] ERROR: com_apple_MobileAsset_MacSoftwareUpdate.xml not found. Check the mount point at /Volumes/Shared Support"
fi
# now we can unmount the dmg
sleep 1
diskutil unmount force "/Volumes/Shared Support"
else
writelog "[check_installer_is_valid] Mounting SharedSupport.dmg failed"
fi
else
# if that fails, fallback to the method for 10.15 or less, which is less accurate
writelog "[check_installer_is_valid] Using DTSDKBuild value from Info.plist"
if [[ -f "$cached_installer_app/Contents/Info.plist" ]]; then
installer_build=$( /usr/bin/defaults read "$cached_installer_app/Contents/Info.plist" DTSDKBuild )
else
writelog "[check_installer_is_valid] Installer Info.plist could not be found!"
fi
fi
# bail out if we did not obtain a build number
if [[ $installer_build ]]; then
# compare the local system's build number with that of the installer app
# if current system is on a beta, we have to assume that this is older than a build number of the same minor version
if /usr/bin/grep -e "[a-z]$" <<< "$system_build"; then
# system is beta
if /usr/bin/grep -e "[a-z]$" <<< "$installer_build"; then
if ! is-at-least "$system_build" "$installer_build"; then
writelog "[check_installer_is_valid] Installer: $installer_build < System: $system_build (both beta): invalid build."
invalid_installer_found="yes"
else
writelog "[check_installer_is_valid] Installer: $installer_build >= System: $system_build (both beta): valid build."
invalid_installer_found="no"
fi
else
if ! is-at-least "${system_build:0:3}" "${installer_build:0:3}"; then
writelog "[check_installer_is_valid] Installer: $installer_build < System: $system_build (beta): invalid build."
invalid_installer_found="yes"
else
writelog "[check_installer_is_valid] Installer: $installer_build >= System: $system_build (beta) : valid build."
invalid_installer_found="no"
fi
fi
elif ! is-at-least "$system_build" "$installer_build"; then
writelog "[check_installer_is_valid] Installer: $installer_build < System: $system_build : invalid build."
invalid_installer_found="yes"
else
writelog "[check_installer_is_valid] Installer: $installer_build >= System: $system_build : valid build."
invalid_installer_found="no"
fi
else
writelog "[check_installer_is_valid] Build of existing installer could not be found, so it is assumed to be invalid."
invalid_installer_found="yes"
fi
working_macos_app="$cached_installer_app"
}
# -----------------------------------------------------------------------------
# Check the validity of an installer pkg.
# packages generated by mist using this script have the name
# InstallAssistant-VERSION-BUILD.pkg
# Extracting an actual version from the package is slow as the entire package
# must be unpackaged to read the PackageInfo file, so we just grab it from the
# filename instead, as mist already did the check.
# -----------------------------------------------------------------------------
check_installer_pkg_is_valid() {
writelog "[check_installer_pkg_is_valid] Checking validity of $cached_installer_pkg."
installer_pkg_build=$( basename "$cached_installer_pkg" | sed 's|.pkg||' | cut -d'-' -f3 )
# compare the local system's build number with that of InstallAssistant.pkg
if ! is-at-least "$system_build" "$installer_pkg_build"; then
writelog "[check_installer_pkg_is_valid] Installer: $installer_pkg_build < System: $system_build : invalid build."
working_installer_pkg="$cached_installer_pkg"
invalid_installer_found="yes"
else
writelog "[check_installer_pkg_is_valid] Installer: $installer_pkg_build >= System: $system_build : valid build."
working_installer_pkg="$cached_installer_pkg"
invalid_installer_found="no"
fi
}
# -----------------------------------------------------------------------------
# Check that a newer installer is available.
# Used with --update.
# This requires mist, so we first check if this is on the system and download
# if not.
# We are using mist to list all available installers, with
# options for different catalogs, and whether to include betas or
# not.
# -----------------------------------------------------------------------------
check_newer_available() {
# Download mist if not present
check_for_mist
# define mist export file location
mist_export_file="$workdir/mist-list.json"
# now clear the variables and build the download command
mist_args=()
mist_args+=("list")
mist_args+=("installer")
mist_args+=("--export")
mist_args+=("$mist_export_file")
# set the search restriction based on --os, --version or --sameos
if [[ $prechosen_version ]]; then
writelog "[check_newer_available] Checking that selected version '$prechosen_version' is available"
mist_args+=("$prechosen_version")
elif [[ $prechosen_os ]]; then
# to avoid a bug where mist-cli does a glob search for the major version, convert it to the name (this is resolved in mist-cli 2.0 but will leave here for now to avoid problems with older installations)
prechosen_os_name=$(convert_os_to_name "$prechosen_os")
writelog "[check_newer_available] Restricting to selected OS '$prechosen_os'"
mist_args+=("$prechosen_os_name")
fi
if [[ "$skip_validation" != "yes" ]]; then
mist_args+=("--compatible")
fi
mist_args+=("--latest")
# set alternative catalog if selected
if [[ $catalogurl ]]; then
writelog "[check_newer_available] Non-standard catalog URL selected"
mist_args+=("--catalog-url")
mist_args+=("$catalogurl")
elif [[ $catalog ]]; then
darwin_version=$(get_darwin_from_os_version "$catalog")
get_catalog
writelog "[check_newer_available] Non-default catalog selected (darwin version $darwin_version)"
mist_args+=("--catalog-url")
mist_args+=("${catalogs[$darwin_version]}")
fi
# include betas if selected
if [[ $beta == "yes" ]]; then
writelog "[check_newer_available] Beta versions included"
mist_args+=("--include-betas")
fi
# run in no-ansi mode which is less pretty but better for our logs
mist_args+=("--no-ansi")
# run mist with --list and then interrogate the plist
if "$mist_bin" "${mist_args[@]}" ; then
newer_build_found="no"
if [[ -f "$mist_export_file" ]]; then
available_build=$( ljt 0.build "$mist_export_file" 2>/dev/null )
if [[ "$available_build" ]]; then
if [[ $installer_pkg_build ]]; then
echo "Comparing latest build found ($available_build) with cached pkg installer build ($installer_pkg_build)"
if ! is-at-least "$available_build" "$installer_pkg_build"; then
newer_build_found="yes"
fi
else
echo "Comparing latest build found ($available_build) with cached installer build ($installer_build)"
if ! is-at-least "$available_build" "$installer_build"; then
newer_build_found="yes"
fi
fi
fi
if [[ $newer_build_found == "yes" ]]; then
writelog "[check_newer_available] Newer installer found."
do_overwrite_existing_installer=1
else
writelog "[check_newer_available] No newer builds found"
fi
else
writelog "[check_newer_available] ERROR reading output from mist, cannot continue"
exit 1
fi
else
writelog "[check_newer_available] ERROR running mist, cannot continue"
exit 1
fi
}
# -----------------------------------------------------------------------------
# Check that the password entered matches the actual password.
# The password is required on Apple Silicon Mac (Thanks to Dan Snelson).
# -----------------------------------------------------------------------------
check_password() {
user="$1"
password="$2"
password_matches=$( /usr/bin/dscl /Search -authonly "$user" "$password" )
if [[ -z "$password_matches" ]]; then
writelog "[check_password] Success: the password entered is the correct login password for $user."
password_check="pass"
else
writelog "[check_password] ERROR: The password entered is NOT the login password for $user."
password_check="fail"
/usr/bin/afplay "/System/Library/Sounds/Basso.aiff"
fi
}
# -----------------------------------------------------------------------------
# Check if device is on battery or AC power.
# If not, and our power_wait_timer is above 1, allow user to connect to power
# for the specified time period.
# Acknowledgements: https://github.com/kc9wwh/macOSUpgrade/blob/master/macOSUpgrade.sh
# -----------------------------------------------------------------------------
check_power_status() {
# default power_wait_timer to 60 seconds
if [[ ! $power_wait_timer ]]; then
power_wait_timer=60
fi
if /usr/bin/pmset -g ps | /usr/bin/grep "AC Power" > /dev/null ; then
writelog "[check_power_status] OK - AC power detected"
elif [[ $silent ]]; then
writelog "[check_power_status] ERROR - No AC power detected, cannot continue."
echo
exit 1
else
if [[ $min_battery_check ]]; then
# set a sensible absolute minimum battery percentage if using min battery check
if ((min_battery_check < 15)); then
min_battery_check=15
fi
writelog "[check_power_status] Minimum battery percentage is set to $min_battery_check"
# check current internal battery percentage
battery_percentage=$(/usr/bin/pmset -g batt | /usr/bin/grep InternalBattery-0 | /usr/bin/awk '{print $3}' | /usr/bin/sed 's|%;||' 2>/dev/null)
# check that the battery has a higher percentage remaining than the minimum set
if ((battery_percentage > min_battery_check)); then
writelog "[check_power_status] OK - battery power is at $battery_percentage"
return
else
writelog "[check_power_status] WARNING - battery power is at $battery_percentage"
fi
fi
writelog "[check_power_status] WARNING - No AC power detected"
# set the dialog command arguments
get_default_dialog_args "utility"
dialog_args=("${default_dialog_args[@]}")
# original icon: ${dialog_confirmation_icon}
dialog_args+=(
"--title"
"${(P)dialog_power_title}"
"--icon"
"${dialog_confirmation_icon}"
"--overlayicon"
"SF=bolt.slash.fill,colour=red"
"--iconsize"
"${dialog_icon_size}"
"--message"
"${(P)dialog_power_desc}"
"--timer"
"${power_wait_timer}"
)
# run the dialog command (stderr to dev/null to prevent Xfont errors)
"$dialog_bin" "${dialog_args[@]}" 2>/dev/null & sleep 0.1
# now count down while checking for power
while [[ "$power_wait_timer" -gt 0 ]]; do
if /usr/bin/pmset -g ps | /usr/bin/grep "AC Power" > /dev/null ; then
writelog "[check_power_status] OK - AC power detected"
# quit dialog
writelog "[check_power_status] Sending quit message to dialog log ($dialog_log)"
echo "quit:" >> "$dialog_log"
return
fi
sleep 1
((power_wait_timer--))
done
# quit dialog
writelog "[check_power_status] Sending quit message to dialog log ($dialog_log)"
echo "quit:" >> "$dialog_log"
# set the dialog command arguments
get_default_dialog_args "utility"
dialog_args=("${default_dialog_args[@]}")
dialog_args+=(
"--title"
"${(P)dialog_power_title}"
"--icon"
"${dialog_confirmation_icon}"
"--iconsize"
"${dialog_icon_size}"
"--overlayicon"
"SF=powerplug.fill,colour=red"
"--message"
"${(P)dialog_nopower_desc}"
)
# run the dialog command
"$dialog_bin" "${dialog_args[@]}" 2>/dev/null
writelog "[check_power_status] ERROR - No AC power detected after waiting for ${power_wait_timer}s, cannot continue."
echo
exit 1
fi
}
# -----------------------------------------------------------------------------
# Confirmation dialogue.
# Called when --confirm option is used.
# Not used in --silent mode.
# -----------------------------------------------------------------------------
confirm() {
# options
if [[ "$erase" == "yes" ]]; then
local dialog_title="${(P)dialog_erase_title}"
local dialog_message="${(P)dialog_erase_confirmation_desc}"
else
local dialog_title="${(P)dialog_reinstall_title}"
local dialog_message="${(P)dialog_reinstall_confirmation_desc}"
fi
# set the dialog command arguments
get_default_dialog_args "utility"
dialog_args=("${default_dialog_args[@]}")
dialog_args+=(
"--title"
"$dialog_title"
"--icon"
"${dialog_confirmation_icon}"
"--iconsize"
"${dialog_icon_size}"
"--overlayicon"
"SF=person.fill.checkmark,colour=red"
"--message"
"$dialog_message"
"--button1text"
"${(P)dialog_confirmation_button}"
"--button2text"
"${(P)dialog_cancel_button}"
)
# run the dialog command
"$dialog_bin" "${dialog_args[@]}" 2>/dev/null
confirmation=$?
if [[ "$confirmation" == "2" ]]; then
writelog "[$script_name] User DECLINED erase-install or reinstall"
exit 0
elif [[ "$confirmation" == "0" ]]; then
writelog "[$script_name] User CONFIRMED erase-install or reinstall"
else
writelog "[$script_name] User FAILED to confirm erase-install or reinstall"
exit 1
fi
}
# -----------------------------------------------------------------------------
# convert OS major version to name
# -----------------------------------------------------------------------------
convert_os_to_name () {
local os_name
case "$1" in
"11") os_name="Big Sur"
;;
"12") os_name="Monterey"
;;
"13") os_name="Ventura"
;;
"14") os_name="Sonoma"
;;
"15") os_name="Sequoia"
;;
*) os_name="$1"
;;
esac
echo "$os_name"
}
# -----------------------------------------------------------------------------
# convert OS major version to name
# -----------------------------------------------------------------------------
convert_name_to_os () {
local os_major_version
case "$1" in
"Big Sur") os_major_version="11"
;;
"Monterey") os_major_version="12"
;;
"Ventura") os_major_version="13"
;;
"Sonoma") os_major_version="14"
;;
"Sequoia") os_major_version="15"
;;
*) os_major_version="$1"
;;
esac
echo "$os_major_version"
}
# -----------------------------------------------------------------------------
# Create a LaunchDaemon that runs startosinstall.
# -----------------------------------------------------------------------------
create_launchdaemon_to_run_startosinstall () {
local install_arg
# Name of LaunchDaemon
# set label and file name for LaunchDaemon
plist_label="$pkg_label.startosinstall"
launch_daemon="/Library/LaunchDaemons/$plist_label.plist"
# Create the plist
if [[ -f "$launch_daemon" ]]; then
rm "$launch_daemon"
fi
/usr/libexec/PlistBuddy -c "Add :Label string '$plist_label'" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :RunAtLoad bool YES" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :LaunchOnlyOnce bool YES" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :StandardInPath string '$pipe_input'" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :StandardOutPath string '$pipe_output'" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :StandardErrorPath string '$pipe_output'" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments array" "$launch_daemon"
i=0
for install_arg in "${combined_args[@]}"; do
/usr/libexec/PlistBuddy -c "Add :ProgramArguments:$i string '$install_arg'" "$launch_daemon"
(( i++ ))
done
}
# -----------------------------------------------------------------------------
# Create a LaunchDaemon that removes the working directory after a reboot.
# This is used with the --cleanup-after-use option.
# -----------------------------------------------------------------------------
create_launchdaemon_to_remove_workdir () {
# Name of LaunchDaemon
plist_label="$pkg_label.remove"
launch_daemon="/Library/LaunchDaemons/$plist_label.plist"
# Create the plist
/usr/libexec/PlistBuddy -c "Add :Label string '$plist_label'" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :RunAtLoad bool YES" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :LaunchOnlyOnce bool YES" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments array" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments:0 string '/bin/rm'" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments:1 string '-Rf'" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments:2 string '$workdir'" "$launch_daemon"
/usr/libexec/PlistBuddy -c "Add :ProgramArguments:3 string '$launch_daemon'" "$launch_daemon"
/usr/sbin/chown root:wheel "$launch_daemon"
/bin/chmod 644 "$launch_daemon"
}
# -----------------------------------------------------------------------------
# Create a pipe
# -----------------------------------------------------------------------------
create_pipe() {
local pipe_name=${1}
local pipe_file
pipe_file=$( /usr/bin/mktemp -u -t "$pipe_name" || exit 12 )
/usr/bin/mkfifo -m go-rw "$pipe_file" || exit 13
echo "$pipe_file"
return 0
}
# -----------------------------------------------------------------------------
# Show progress information in DEPNotify while the installer is being
# downloaded or prepared, or during reboot-delay, thanks to @andredb90.
# -----------------------------------------------------------------------------
dialog_progress() {
last_progress_value=0
current_progress_value=0
# initialise progress messages
writelog "[dialog_progress] Sending to dialog: progresstext:"
echo "progresstext: " >> "$dialog_log"
echo "progress: 0" >> "$dialog_log"
if [[ "$1" == "startosinstall" ]]; then
# Wait for the preparing process to start and set the progress bar to 100 steps
until grep -q "Preparing to run macOS Installer..." "$LOG_FILE" ; do
sleep 0.1
done
writelog "[dialog_progress] Sending to dialog: progresstext: Preparing to run macOS Installer..."
echo "progresstext: Preparing to run macOS Installer..." >> "$dialog_log"
until grep -q "Preparing: \d" "$LOG_FILE" ; do
if grep -q "Error: could not get authorization..." "$LOG_FILE"; then
writelog "[dialog_progress] ERROR: startosinstall authorization failed"
exit 20
fi
sleep 2
done
echo "progress: 0" >> "$dialog_log"
# Until at least 100% is reached, calculate the preparing progress and move the bar accordingly
until [[ $current_progress_value -ge 100 ]]; do
until [[ $current_progress_value -gt $last_progress_value ]]; do
log_value=$(tail -1 "$LOG_FILE" | awk 'END{print substr($NF, 1, length($NF)-3)}')
# check we got a number
if [ "$log_value" -eq "$log_value" ] 2>/dev/null; then
current_progress_value="$log_value"
fi
sleep 1
done
echo "progresstext: Preparing macOS Installer ($current_progress_value%)" >> "$dialog_log"
echo "progress: $current_progress_value" >> "$dialog_log"
last_progress_value=$current_progress_value
done
elif [[ "$1" == "mist" ]]; then
# if mist runs in quiet mode we cannot display download progress
if [[ "$quiet" == "yes" ]]; then
echo "progresstext: Downloading macOS installer..." >> "$dialog_log"
else
# Wait for a search message to appear
until grep -q "SEARCH" "$LOG_FILE" ; do
sleep 1
done
writelog "[dialog_progress] Sending to dialog: progresstext: Searching for a valid macOS installer..."
echo "progresstext: Searching for a valid macOS installer..." >> "$dialog_log"
# Wait for a Found message to appear
until grep -q "Found \[" "$LOG_FILE" ; do
sleep 1
done
dialog_found_installer=$(/usr/bin/grep "Found \[" "$LOG_FILE" | sed 's/.*Found \[.*\] //' | sed 's/ \[.*\]//')
writelog "[dialog_progress] Sending to dialog: progresstext: Found $dialog_found_installer"
echo "progresstext: Found $dialog_found_installer" >> "$dialog_log"
# Wait for the download to start and set the progress bar to 100 steps