-
Notifications
You must be signed in to change notification settings - Fork 208
/
api
executable file
·3943 lines (3306 loc) · 190 KB
/
api
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/bash
#This script is the backbone of Pi-Apps. It hosts functions that all other scripts depend upon.
#By default, this file is hard to read. But there's an easy way to fix that. In your code editor, find the option to "Fold All".
#In Geany, this option can be found in the Document toolbar.
#Botspot has found it helpful to create a keyboard shortcut to do this quickly. You may find it helpful too.
#output functions below
error() { #red text and exit 1
echo -e "\e[91m$1\e[0m" 1>&2
exit 1
}
warning() { #yellow text
echo -e "\e[93m\e[5m◢◣\e[25m WARNING: $1\e[0m" 1>&2
}
status() { #cyan text to indicate what is happening
#detect if a flag was passed, and if so, pass it on to the echo command
if [[ "$1" == '-'* ]] && [ ! -z "$2" ];then
echo -e $1 "\e[96m$2\e[0m" 1>&2
else
echo -e "\e[96m$1\e[0m" 1>&2
fi
}
status_green() { #announce the success of a major action
echo -e "\e[92m$1\e[0m" 1>&2
}
debug() { #an echo command that only runs when debug mode is on
[ "$pi_apps_debug" = true ] && echo "$1"
}
generate_logo() { #display colorized Pi-Apps logo in terminal
#ANSI color codes: https://misc.flogisoft.com/bash/tip_colors_and_formatting
#Search for unicode characters: https://unicode-search.net - search for "block" and "quadrant"
# foreground colors
blue1='\e[38;5;75m'
blue2='\e[38;5;26m'
blue3='\e[38;5;21m'
blue4='\e[38;5;93m'
green='\e[38;5;46m'
darkgreen='\e[38;5;34m'
red='\e[38;5;197m'
white='\e[97m'
black='\e[30m'
default='\e[39m'
# background colors
bg_default='\e[49m'
bg_black='\e[40m'
bg_white='\e[107m'
#complex logo requires Unicode 13 support (libicu66+)
#this is available in Ubuntu 20.04+ and Debian 11+
#use simpler logo when not supported to fix issue https://github.com/Botspot/pi-apps/issues/1441
if [ -f /usr/lib/aarch64-linux-gnu/libicudata.so ]; then
local version="$(readlink -f /usr/lib/aarch64-linux-gnu/libicudata.so | sed -n 's/.*libicudata.so.//p')"
elif [ -f /usr/lib/arm-linux-gnueabihf/libicudata.so ]; then
local version="$(readlink -f /usr/lib/arm-linux-gnueabihf/libicudata.so | sed -n 's/.*libicudata.so.//p')"
elif find /usr/lib/aarch64-linux-gnu/libicudata.so.* &>/dev/null; then
local version="$(find /usr/lib/aarch64-linux-gnu/libicudata.so.* | head -1 | sed -n 's+^/usr/lib/aarch64-linux-gnu/libicudata.so.++p')"
elif find /usr/lib/arm-linux-gnueabihf/libicudata.so.* &>/dev/null; then
local version="$(find /usr/lib/arm-linux-gnueabihf/libicudata.so.* | head -1 | sed -n 's+^/usr/lib/arm-linux-gnueabihf/libicudata.so.++p')"
else
local version="65"
fi
if (( $(echo "$version >= 66" | bc -l) )); then
bg_black='\e[48;2;10;10;10m'
echo -e "${bg_default} \e[38;2;5;220;75m🭊\e[38;2;4;150;29m🬹🬹🬹\e[38;2;6;188;64m🬿${default} ${darkgreen} ${default}
\e[38;2;83;213;255m🭈🬭\e[38;2;83;214;255m🬭\e[38;2;5;220;75m\e[48;2;83;212;255m🬎${bg_default}\e[38;2;84;201;251m🬭\e[38;2;84;190;248m🬭\e[38;2;85;178;244m🬭\e[38;2;6;188;64m\e[48;2;86;168;241m🬎${bg_default}\e[38;2;87;154;237m🬭🬭\e[38;2;87;136;231m🬽${default} ${darkgreen} ${default}
\e[38;2;83;213;255m${bg_black}▋ \e[38;2;255;38;101m▄ \e[38;2;255;28;92m▄ \e[38;2;255;13;83m▄\e[38;2;89;114;225m 🮉${bg_default}${default} █▀▀🭍 ▄ 🭋🭡🭖🭀 ${darkgreen} ${black} ${darkgreen} ${black} ${darkgreen} ${default}
\e[38;2;85;191;249m${bg_black}▋ \e[38;2;255;13;85m▄ \e[38;2;255;0;75m▄ \e[38;2;246;0;73m▄\e[38;2;90;83;215m 🮉${bg_default}${default} █▄▄🭞 ▄ ${blue3}▄▄${default} 🭅▙▟🭐 █▀▀🭍 █▀▀🭍 🭂🬰🬰🬰 ${darkgreen} ${black} ${darkgreen} ${black} ${darkgreen} ${default}
\e[38;2;86;164;240m${bg_black}▋ \e[38;2;249;0;73m▄ \e[38;2;239;0;69m▄ \e[38;2;229;0;66m▄\e[38;2;92;58;207m 🮉${bg_default}${default} █ █ 🭋🭡 🭖🭀█▄▄🭞 █▄▄🭞 ▄▄▄🭞 ${darkgreen} ${black} ${darkgreen} ${default}
\e[38;2;87;137;232m🭕${bg_black}🭏\e[38;2;89;111;224m🬭\e[38;2;89;100;220m🬭\e[38;2;90;89;217m🬭\e[38;2;91;76;213m🬭\e[38;2;92;68;211m🬭\e[38;2;92;59;208m🬭\e[38;2;92;56;207m🬭🭄${bg_default}🭠${default} █ █ ${darkgreen} ${black} ${darkgreen} ${default}
\e[0m ${darkgreen} ${black} ${darkgreen} ${default}
${darkgreen} ${black} ${darkgreen} ${black} ${darkgreen} ${default}"
else
echo -e "${white}${bg_default} ${green}▅${darkgreen}▅▅▅${green}▅${default} ${darkgreen} ${default}
${blue1}▂▂▂${green}\e[48;5;26m\e[7m▂\e[27m${bg_default}${blue2}▂▂▂${blue3}${green}\e[48;5;26m\e[7m▂\e[27m${bg_default}${blue3}▂▂▂${white}${default} ${darkgreen} ${default}
${bg_black}${blue1}▌ ${red}▄ ▄ ▄${blue3} ▐${bg_default}${default} █▀▀◣ ▄ ◢▀▀◣ ${darkgreen} ${black} ${darkgreen} ${black} ${darkgreen} ${default}
${bg_black}${blue2}▌ ${red}▄ ▄ ▄${blue3} ▐${bg_default}${default} █▄▄◤ ▄ ${blue3}▄▄${default} █▄▄█ █▀▀◣ █▀▀◣ ◢\e[7m━━━\e[27m ${darkgreen} ${black} ${darkgreen} ${black} ${darkgreen} ${default}
${bg_black}${blue2}▌ ${red}▄ ▄ ▄${blue4} ▐${bg_default}${default} █ █ █ █ █▄▄◤ █▄▄◤ ▄▄▄◤ ${darkgreen} ${black} ${darkgreen} ${default}
${blue3}◥${bg_black}▃▃▃▃${blue4}▃▃▃▃▃${bg_default}◤${default} █ █ ${darkgreen} ${black} ${darkgreen} ${default}
\e[0m ${darkgreen} ${black} ${darkgreen} ${default}
${darkgreen} ${black} ${darkgreen} ${black} ${darkgreen} ${default}"
fi
}
#end of output functions
add_english() { #add en_US locale for more accurate error
if [ "$(cat /usr/share/i18n/SUPPORTED | grep -o 'en_US.UTF-8' )" == "en_US.UTF-8" ]; then
locale=$(locale -a | grep -oF 'en_US.utf8')
if [ "$locale" != 'en_US.utf8' ]; then
status "Adding en_US locale for better logging... "
sudo sed -i '/en_US.UTF-8/s/^#[ ]//g' /etc/locale.gen
sudo locale-gen
fi
else
warning "en_US locale is not available on your system. This may cause bad logging experience."
fi
export LANG="en_US.UTF-8"
export LANGUAGE="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"
}
#package functions
package_info() { #list everything dpkg knows about the $1 package. Note: the package has to be installed for this to show anything.
local package="$1"
[ -z "$package" ] && error "package_info(): no package specified!"
#list lines in /var/lib/dpkg/status between the package name and the next empty line (empty line is then removed)
sed -n -e '/^Package: '"$package"'$/,/^$/p' /var/lib/dpkg/status | head -n -1
true #this may exit with code 141 if the pipe was closed early (to be expected with grep -v)
}
package_installed() { #exit 0 if $1 package is installed, otherwise exit 1
local package="$1"
[ -z "$package" ] && error "package_installed(): no package specified!"
#find the package listed in /var/lib/dpkg/status
#package_info "$package"
#directly search /var/lib/dpkg/status
grep -x "Package: $package" /var/lib/dpkg/status -A 2 | grep -qxF 'Status: install ok installed'
}
package_available() { #determine if the specified package-name exists in a local repository for the current dpkg architecture
local package="$(awk -F: '{print $1}' <<<"$1")"
local dpkg_arch="$(awk -F: '{print $2}' <<<"$2")"
[ -z "$dpkg_arch" ] && dpkg_arch="$(dpkg --print-architecture)"
[ -z "$package" ] && error "package_available(): no package name specified!"
local output="$(apt-cache policy "$package":"$dpkg_arch" | grep "Candidate:")"
if [ -z "$output" ]; then
return 1
elif echo "$output" | grep -q "Candidate: (none)"; then
return 1
else
return 0
fi
}
package_dependencies() { #outputs the list of dependencies for the $1 package
local package="$1"
[ -z "$package" ] && error "package_dependencies(): no package specified!"
#find the package listed in /var/lib/dpkg/status
package_info "$package" | grep '^Depends: ' | sed 's/^Depends: //g'
}
package_installed_version() { #returns the installed version of the specified package-name.
local package="$1"
[ -z "$package" ] && error "package_installed_version(): no package specified!"
#find the package listed in /var/lib/dpkg/status
package_info "$package" | grep '^Version: ' | awk '{print $2}'
}
package_latest_version() { #returns the latest available versions of the specified package-name. Doesn't matter if it's installed or not.
local package="$1"
[ -z "$package" ] && error "package_latest_version(): no package specified!"
# use slower but more accurate apt list command to get package version for current architecture
apt-cache policy "$package" 2>/dev/null | grep "Candidate: " | awk '{print $2}'
#grep -rx "Package: $package" /var/lib/apt/lists --exclude="lock" --exclude-dir="partial" --after 4 | grep -o 'Version: .*' | awk '{print $2}' | sort -rV | head -n1
}
package_is_new_enough() { #check if the $1 package has an available version greater than or equal to $2
local package="$1"
[ -z "$package" ] && error "package_is_new_enough(): no package specified!"
local compare_version="$2"
[ -z "$package" ] && error "package_is_new_enough(): no comparison version number specified!"
#determine the latest available version for the specified package
local package_version="$(package_latest_version "$package")"
#if version value not found, return 1 now
if [ -z "$package_version" ];then
return 1
fi
#given both the package_version and compare_version, see if the greater of the two is the available package's version
if [ "$(echo "$package_version"$'\n'"$compare_version" | sort -rV | head -n1)" == "$package_version" ];then
#if so, indicate success
return 0
else
return 1
fi
}
anything_installed_from_uri_suite_component() { #Given an apt repository uri, suite, and component, determine if any packages from it are currently installed
local uri="$1"
local suite="$2"
component="$3" #can be left blank
[ -z "$uri" ] && error "anything_installed_from_uri_suite_component: A repository uri must be specified."
[ -z "$suite" ] && error "anything_installed_from_uri_suite_component: A repository suite must be specified."
#find part of path to apt list file(s) to search for
if [ -z "$component" ]; then
local filepath="/var/lib/apt/lists/$(echo "$1" | sed 's+.*://++g' | sed "s,/$,," | tr '/' '_')_$(echo "$suite" | sed "s,/$,," | tr '/' '_')_"
else
local filepath="/var/lib/apt/lists/$(echo "$1" | sed 's+.*://++g' | sed "s,/$,," | tr '/' '_')_dists_$(echo "$suite" | sed "s,/$,," | tr '/' '_')_$(echo "$component" | sed "s,/$,," | tr '/' '_')_"
fi
debug $filepath
#find all relevant package-lists
local repofiles="$(ls ${filepath}*_Packages)"
debug "$repofiles"
#for every repo-file, check if any of them have an installed file
local IFS=$'\n'
local repofile
local installed_packages="$(grep -xF 'Status: install ok installed' /var/lib/dpkg/status -B 2 | grep '^Package: ' | sed 's/^Package: //g' | sort)"
for repofile in $repofiles ;do
#search the repo-file for installed packages
local packages_in_repo="$(grep '^Package: ' "$repofile" | awk '{print $2}' | sort)"
local apt_cache_policy_output="$(echo "$packages_in_repo" | list_intersect "$installed_packages" | tr '\n' ' ' | xargs -r apt-cache policy)"
#check if any installed packages also found on this repo are actually installed from this repo
if [ -z "$component" ]; then
echo "$apt_cache_policy_output" | grep -B1 "$(echo "$uri" | sed 's+.*://++g' | sed "s,/$,,") $suite" | awk '{print $1}' | grep -Fq '***' && return 0
else
echo "$apt_cache_policy_output" | grep -B1 "$(echo "$uri" | sed 's+.*://++g' | sed "s,/$,,") $suite/$component" | awk '{print $1}' | grep -Fq '***' && return 0
fi
done
return 1
}
remove_repofile_if_unused() { #Given a sources.list.d file, delete it if nothing from that repository is currently installed. Deletion skipped if $2 is 'test'
local file="$1"
local testmode="$2"
local key="$3"
[ -z "$file" ] && error "remove_repo_if_unused: no sources.list.d file specified!"
#return now if the list file does not exist
[ -f "$file" ] || return 0
#set default to not in use
local in_use=0
if [ "${file##*.}" == "list" ]; then
# determine what uri, suite, and components are in a file
local lines="$(cat "$file" | grep "^deb " | sed 's/^deb // ; s/\[.*\]//')"
local IFS=$'\n'
for line in $lines ;do
local uri="$(echo "$line" | awk '{print $1}')"
local suite="$(echo "$line" | awk '{print $2}')"
local components="$(echo "$line" | awk '{$1=$2=""; print $0}')"
local IFS=' '
if [ -z "$components" ]; then
debug "$uri $suite"
if anything_installed_from_uri_suite_component "$uri" "$suite";then
in_use=1
break 1
fi
else
for component in $components ;do
debug "$uri $suite $component"
if anything_installed_from_uri_suite_component "$uri" "$suite" "$component";then
in_use=1
break 2
fi
done
fi
local IFS=$'\n'
done
elif [ "${file##*.}" == "sources" ]; then
#find empty lines (empty line, line with all spaces, or line with all tabs) in the file that separate stanzas. empty lines are not allowed between fields within a stanza
#https://manpages.ubuntu.com/manpages/jammy/en/man5/deb822.5.html
local empty_lines="$(grep -P -n '^$|^ +$|^\t+$' "$file" | awk '{print $1}' | sed 's/:*//g')"
#get number of lines in file
local num_lines=$(wc -l "$file" | awk '{print $1}')
#always add last line to empty lines
if [ -z "$empty_lines" ]; then
empty_lines="$num_lines"
else
empty_lines+=$'\n'"$num_lines"
fi
debug "$empty_lines"
#parse each stanza, starting at line 1
local IFS=$'\n'
local line_start=1
for line_end in $empty_lines ;do
# if Enabled: no, continue to next loop iteration
sed -n "$line_start","$line_end"p "$file" | grep -q '^Enabled: no' && continue
# determine what uri, suite, and components are in a file
#each stanza can only have one matching section. if there are multiple matches the last one is used
#case should be ignored for fields
local uris="$(sed -n "$line_start","$line_end"p "$file" | grep -v '^#' | grep -i '^URIs:' | sed 's/URIs://Ig' | awk '{$1=$1};1' | tail -1)"
local suites="$(sed -n "$line_start","$line_end"p "$file" | grep -v '^#' | grep -i '^Suites:' | sed 's/Suites://Ig' | awk '{$1=$1};1' | tail -1)"
local components="$(sed -n "$line_start","$line_end"p "$file" | grep -v '^#' | grep -i '^Components:' | sed 's/Components://Ig' | awk '{$1=$1};1' | tail -1)"
local IFS=' '
for uri in $uris ;do
for suite in $suites ;do
if [ -z "$components" ]; then
debug "$uri $suite"
if anything_installed_from_uri_suite_component "$uri" "$suite";then
in_use=1
break 3
fi
else
for component in $components ;do
debug "$uri $suite $component"
if anything_installed_from_uri_suite_component "$uri" "$suite" "$component";then
in_use=1
break 4
fi
done
fi
done
done
local IFS=$'\n'
line_start="$line_end"
done
else
error "$file was not of apt list or sources type"
fi
if [ "$testmode" == test ] && [ "$in_use" == 0 ];then
echo "The given repository is not in use and can be deleted:"$'\n'"$file" 1>&2
elif [ "$testmode" == test ];then
echo "At least one package is preventing the repo from being removed"
elif [ "$in_use" == 0 ];then
status "Removing the $(basename "$file" | sed 's/.list$//g' | sed 's/.sources$//g') repo as it is not being used"
sudo rm -f "$file"
[ -f "$key" ] && sudo rm -f "$key" || true
fi
}
#apt functions
apt_lock_wait() { #Wait until other apt processes are finished before proceeding
#make sure english locale is added first
add_english
#check if sudo needs a password currently. this prevents sudo asking for a password in the below fuser command which would have "Waiting until APT locks are released... " written after it after 5 seconds
#the result would look like something in the terminal to a user "[sudo] password for USER: Waiting until APT locks are released... " which might be confusing
#this is very often the first command that pi-apps scripts run that requires sudo privileges from the user
#passwordless sudo (like on piOS) will always skip the contents of the if
if ! sudo -n true ; then
# sudo needs a password so prompt the user to give one before running other commands
sudo echo > /dev/null
fi
#in a background subprocess, after 5 seconds, say "Waiting until APT locks are released... "
(sleep 5; echo -n "Waiting until APT locks are released... ") &
local pid=$!
while [ ! -z "$(sudo fuser /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock /var/log/unattended-upgrades/unattended-upgrades.log /var/lib/dpkg/lock-frontend 2>/dev/null)" ];do
sleep 1
done
#Try to install a non-existent package to see if apt fails due to a lock-file. Repeat until no errors mention 'Could not get lock'
#NOTE: after apt 3.0 this output may change. Refer to git blame for this line for more info.
while sudo -E apt install lkqecjhxwqekc 2>&1 | grep -q 'Could not get lock' ;do
sleep 1
done
#If the background process finished, then that means the "waiting until" message was displayed. This means the kill command will return 1, so echo Done
kill $pid &>/dev/null || echo "Done"
}
less_apt() { #remove unwanted lines from apt output
grep --line-buffered -v "apt does not have a stable CLI interface.\|Reading package lists...\|Building dependency tree\|Reading state information...\|Need to get\|Selecting previously unselected package\|Preparing to unpack\|Setting up \|Processing triggers for \|^$"
}
apt_update() { #run an apt update with error-checking and minimal output
apt_lock_wait
status "Running \e[7msudo apt update\e[27m..."
set -o pipefail
local output="$(sudo -E apt update --allow-releaseinfo-change "$@" 2>&1 | less_apt | tee /dev/stderr)"
local exitcode=$?
status "apt update complete."
#inform user about autoremovable packages
if [ ! -z "$(echo "$output" | grep 'autoremove to remove them' )" ];then
echo -e "\e[33mSome packages are unnecessary.\e[39m Please consider running \e[4msudo a\e[0mp\e[4mt autoremove\e[0m."
fi
#inform user packages are upgradeable
if [ ! -z "$(echo "$output" | grep 'packages can be upgraded' )" ];then
echo -e "\e[33mSome packages can be upgraded.\e[39m Please consider running \e[4msudo a\e[0mp\e[4mt full-u\e[0mpg\e[4mrade\e[0m."
elif [ ! -z "$(echo "$output" | grep 'package can be upgraded' )" ];then
echo -e "\e[33mOne package can be upgraded.\e[39m Please consider running \e[4msudo a\e[0mp\e[4mt full-u\e[0mpg\e[4mrade\e[0m."
fi
#exit on apt error
local errors="$(echo "$output" | grep '^[(E)|(Err]:')"
if [ $exitcode != 0 ] || [ ! -z "$errors" ];then
echo -e "\e[91mFailed to run \e[4msudo apt update\e[0m\e[39m!"
echo -e "APT reported these errors:\n\e[91m$errors\e[39m"
#run some apt error diagnosis
echo "$output"
exit 1
fi
return 0
}
repo_add() { #add local packages to the /tmp/pi-apps-local-packages repository
#given local deb file(s), make a local apt repository in /tmp/pi-apps-local-packages
#see: https://unix.stackexchange.com/questions/87130/how-to-quickly-create-a-local-apt-repository-for-random-packages-using-a-debian
#and: https://serverfault.com/questions/447457/use-apt-get-source-on-a-debian-repo-without-using-etc-apt-source-list
#and: https://askubuntu.com/questions/382664/use-custom-directory-for-apt-get
#use this flag for apt commands to use the local repository: -o Dir::Etc::SourceList=/tmp/pi-apps-local-packages/source.list
#ensure the repo-folder exists
mkdir -p /tmp/pi-apps-local-packages || error "repo_add(): failed to create folder /tmp/pi-apps-local-packages"
#move every mentioned deb file to it
for file in "$@"; do
mv -f "$file" /tmp/pi-apps-local-packages || error "repo_add(): failed to move '$file' to the repository: /tmp/pi-apps-local-packages"
done
}
repo_refresh() { #index the pi-apps local apt repository
[ -d /tmp/pi-apps-local-packages ] || error "repo_update(): cannot index the repository - it's missing! /tmp/pi-apps-local-packages"
#index the repository by creating a Packages file
(cd /tmp/pi-apps-local-packages && apt-ftparchive packages . > Packages) || error "repo_update(): apt-ftparchive failed to index the repository: /tmp/pi-apps-local-packages
The Pi-Apps developers have been receiving a few of these errors recently, but we can't figure out what the problem is without your help. Could you please reach out so we can solve this?"
#Make sure the Packages file actually exists
[ -f /tmp/pi-apps-local-packages/Packages ] || error "repo_update(): apt-ftparchive failed to index the repository: /tmp/pi-apps-local-packages
The Pi-Apps developers have been receiving a few of these errors recently, but we can't figure out what the problem is without your help. Could you please reach out so we can solve this?"
#by default, apt-ftparchive will generate lines like "Filename: ./package-name". This seemed to have caused one error-report, so we remove the "./" to hopefully solve the problem.
sed -i 's+^Filename: \./+Filename: +g' /tmp/pi-apps-local-packages/Packages
# set repo origin name to pi-apps-local-packages - see PR #1986
echo 'APT::FTPArchive::Release {
Origin "pi-apps-local-packages";
};' > /tmp/pi-apps-local-packages/aptftp.conf
#hash the repository by creating a Release file
(cd /tmp/pi-apps-local-packages && apt-ftparchive -c=/tmp/pi-apps-local-packages/aptftp.conf release . > Release) || error "repo_update(): apt-ftparchive failed to hash the repository: /tmp/pi-apps-local-packages
The Pi-Apps developers have been receiving a few of these errors recently, but we can't figure out what the problem is without your help. Could you please reach out so we can solve this?"
#create a source.list for the repository - add pi-apps-local-packages to the top, then add lines from /etc/apt/sources.list
rm -f /tmp/pi-apps-local-packages/source.list
echo "deb [trusted=yes] file:/tmp/pi-apps-local-packages/ ./" > /tmp/pi-apps-local-packages/source.list
cat /etc/apt/sources.list >> /tmp/pi-apps-local-packages/source.list
# remove pi-apps-local-packages priority if it exists
[ -f /etc/apt/preferences.d/pi-apps-local-packages ] && sudo rm -f /etc/apt/preferences.d/pi-apps-local-packages
true
}
repo_rm() { #remove the local apt repository
#wait for other operations to finish before continuing - hopefully this will solve cases when the pi-apps local repository was removed unexpectedly by a second process
apt_lock_wait
rm -rf /tmp/pi-apps-local-packages || sudo rm -rf /tmp/pi-apps-local-packages || error "repo_rm(): failed to remove the local repository: /tmp/pi-apps-local-packages"
#Also remove broken symbolic link to /tmp/pi-apps-local-packages/./Packages
sudo rm -f /var/lib/apt/lists/_tmp_pi-apps-local-packages_._Packages
}
app_to_pkgname() { #given an app-name, convert it to a unique, valid package-name that starts with 'pi-apps-'
local app="$1"
[ -z "$app" ] && error "app_to_pkgname(): no app-name specified"
echo "pi-apps-$(echo "$app" | md5sum | cut -c1-8 | awk '{print $1}')"
}
install_packages() { #Make some packages dependencies of the $app app. Package-names, regex, filenames, and urls are supported.
#array-variable to store custom apt options (for local repositories)
local apt_flags=()
#convert input array to newline-separated string
local IFS=' '
while [ $# -gt 0 ]; do
if [ "$1" == '-t' ];then #pass through -t args to apt: for "-t bookworm-backports"
apt_flags+=('-t' "$2")
shift
shift
else
local packages+="$1
"
shift
fi
done
packages="${packages::-1}" #remove final empty newline
#the $app variable must contain something
[ -z "$app" ] && error 'install_packages function can only be used by apps to install packages. (the $app variable was not set)'
status "Will install these packages: $(tr '\n' ' ' <<<"$packages")"
#variable to remember if the pi-apps-local-packages repository is being used
local using_local_packages=0
repo_rm #remove the local repo, just in case the last operation left it in an unrecoverable state.
#handle regex, urls, local packages
IFS=$'\n'
local package
for package in $packages ;do
#handle local packages (package-name starts with /)
if [[ "$package" == /* ]];then
#status "Handling local package $package"
[ -f "$package" ] || error "install_packages(): Local package does not exist! ($package)"
#determine the package name, package version, and architecture from the file
local dpkg_deb_output="$(dpkg-deb -I "$package")"
local packagename="$(echo "$dpkg_deb_output" | grep "^ Package:" | awk '{print $2}')"
local packageversion="$(echo "$dpkg_deb_output" | grep "^ Version:" | awk '{print $2}')"
local packagearch="$(echo "$dpkg_deb_output" | grep "^ Architecture:" | awk '{print $2}')"
[ -z "$packagename" ] && error "install_packages(): failed to determine a package-name for the file '$package'"
[ -z "$packageversion" ] && error "install_packages(): failed to determine a package-version for the file '$package'"
[ -z "$packagearch" ] && error "install_packages(): failed to determine a package-architecture for the file '$package'"
unset dpkg_deb_output
#foreign arch: add :armhf or :arm64 to the packagename if this local package is of a foreign architecture
if [ "$packagearch" != "$(dpkg --print-architecture)" ] && [ "$packagearch" != "all" ];then
packagename+=":$packagearch"
fi
#add this local package to the pi-apps-local-packages repository
repo_add "$package" || return 1
using_local_packages=1 #remember that the pi-apps-local-packages repository is being used
#replace package filename with name of package
packages="$(echo "$packages" | sed "s|$package|$packagename (>= $packageversion)|")"
#handle urls
elif [[ "$package" == *://* ]];then
#status "Handling url: $package"
local filename="/tmp/$(basename "$(echo "$package" | sed 's+/download$++g')")"
#add .deb extension if filename doesn't end with it.
if [ "${filename: -4}" != ".deb" ]; then
status "$filename is not ending with .deb, renaming it to '${filename}.deb'..."
local filename="${filename}.deb"
fi
for i in {1..3}; do #download retry loop
wget -O "$filename" "$package" || rm -f "$filename"
if [ -f "$filename" ];then
break
fi
warning "Package download failed. (Attempt $i of 3)"
done
[ -f "$filename" ] || error "install_packages(): Downloaded package does not exist! ($filename)"
#determine the package name, package version, and architecture from the file
local dpkg_deb_output="$(dpkg-deb -I "$filename")"
local packagename="$(echo "$dpkg_deb_output" | grep "^ Package:" | awk '{print $2}')"
local packageversion="$(echo "$dpkg_deb_output" | grep "^ Version:" | awk '{print $2}')"
local packagearch="$(echo "$dpkg_deb_output" | grep "^ Architecture:" | awk '{print $2}')"
[ -z "$packagename" ] && error "install_packages(): failed to determine a package-name for the file '$filename'"
[ -z "$packageversion" ] && error "install_packages(): failed to determine a package-version for the file '$filename'"
[ -z "$packagearch" ] && error "install_packages(): failed to determine a package-architecture for the file '$filename'"
unset dpkg_deb_output
#foreign arch: add :armhf or :arm64 to the packagename if this local package is of a foreign architecture
if [ "$packagearch" != "$(dpkg --print-architecture)" ] && [ "$packagearch" != "all" ];then
packagename+=":$packagearch"
fi
#add this local package to the pi-apps-local-packages repository
repo_add "$filename" || return 1
using_local_packages=1 #remember that the pi-apps-local-packages repository is being used
#replace package url with name of package
packages="$(echo "$packages" | sed "s|$package|$packagename (>= $packageversion)|")"
#expand regex (package-name contains *)
elif echo "$package" | grep -q '*' ;then
status "Expanding regex in '${package}'..."
list="$(apt-cache search "$package" | awk '{print $1}' | grep "$(echo "$package" | tr -d '*')")"
#replace package with expanded list
packages="$(echo "$packages" | grep -vF "$package")"$'\n'"$list"
fi
done
#now package list shouldn't contain any '*' characters, urls, local filepaths
if echo "$packages" | grep -q '*';then
error "install_packages(): failed to remove all regex from the package list:\n$packages"
elif [[ "$packages" == *://* ]];then
error "install_packages(): failed to remove all urls from the package list:\n$packages"
elif [[ "$packages" == */* ]];then
error "install_packages(): failed to remove all filenames from the package list:\n$packages"
fi #package list contains no '*' characters, urls, local filepaths
#change the $packages list from newline-delimited to comma-delimited
packages="$(tr '\n' ',' <<<"$packages")"
if [ "$using_local_packages" == 1 ];then
#Initialize the pi-apps-local-packages repository
repo_refresh || return 1
#add this repository to flags to add to apt
apt_flags+=(-o 'Dir::Etc::SourceList=/tmp/pi-apps-local-packages/source.list')
fi
status "Creating an empty apt-package to install the necessary apt packages..."
#to avoid issues with symbols and spaces in app names, we shasum the app name for use in apt
local package_name="$(app_to_pkgname "$app")"
echo "It will be named: $package_name"
#If this app's dummy deb is already installed, add its dependencies to the list.
#this allows install_packages() to be used multiple times in an app's script
if package_installed "$package_name" ;then
local existing_deps="$(package_dependencies "$package_name")"
status "The $package_name package is already installed. Inheriting its dependencies: $(echo "$existing_deps")"
packages+=",""$(echo "$existing_deps" | sed 's/, /,/g')"
fi
{ #create dummy apt package that depends on the packages this app requires
#this stores the comma-separated list of dependency packages. It removes duplicate entries.
local depends="$(echo "$packages" | sed 's/,|/ |/g' | sed 's/|,/| /g' | tr ',' '\n' | sort -u -t' ' -k1,1 | tr '\n' ',' | sed 's/^,//g' | sed 's/,$//g' | sed 's/,/, /g' ; echo)"
rm -rf /tmp/$package_name /tmp/$package_name.deb
mkdir -p /tmp/$package_name/DEBIAN
echo "Maintainer: Pi-Apps team
Name: $app
Description: Dummy package created by pi-apps to install dependencies for the '$app' app
Version: 1.0
Architecture: all
Priority: optional
Section: custom
Depends: $depends
Package: $package_name" > /tmp/$package_name/DEBIAN/control
#fix error report "dpkg-deb: error: control directory has bad permissions 700 (must be >=0755 and <=0775)"
#The two zeros fix error report "dpkg-deb: error: control directory has bad permissions 2755 (must be >=0755 and <=0775)"
sudo chmod -R '00755' /tmp/$package_name
#display the finished "Depends: " line to the user
grep --color=never "^Depends: " /tmp/$package_name/DEBIAN/control
}
#Skip installing the dummy deb if it is already installed and has an identical control file
if package_installed "$package_name" && [ "$(package_info "$package_name" | sort | grep -v '^Status: ')" == "$(cat /tmp/$package_name/DEBIAN/control | sort)" ];then
echo "$package_name is already installed and no changes would be made. Skipping..."
else
#Build .deb file for the dummy package
local output="$(dpkg-deb --build /tmp/$package_name 2>&1)"
if [ $? != 0 ] || [ ! -f /tmp/$package_name.deb ];then
echo ""
echo "$output"
error "install_packages(): failed to create dummy deb ${package_name}!"
fi
#Before apt update, check if local repo still exists
if [ "$using_local_packages" == 1 ] && [ ! -f /tmp/pi-apps-local-packages/Packages ];then
error "User error: Uh-oh, the /tmp/pi-apps-local-packages folder went missing while installing packages.\nThis usually happens if you try to install several apps at the same time in multiple terminals."
fi
local i=0
while true;do #retry loop ; run apt update and apt install again if apt's records of the local repo goes missing
#run an apt update
apt_update "${apt_flags[@]}" || exit 1
#install dummy deb
status "Installing the $package_name package..."
apt_lock_wait
set -o pipefail
local output="$(sudo -E apt install -fy --no-install-recommends --allow-downgrades "${apt_flags[@]}" /tmp/$package_name.deb 2>&1 | less_apt | tee /dev/stderr)"
local exitcode=$?
status "Apt finished."
if [ "$using_local_packages" == 1 ] && [ ! -f /var/lib/apt/lists/_tmp_pi-apps-local-packages_._Packages ] && [ $i != 5 ];then
#another apt update process deleted apt's knowledge of the pi-apps local repo. Warn the user and try again, up to 5 tries.
i=$((i+1))
warning "Local packages failed to install because another apt update process erased apt's knowledge of the pi-apps local repository.\nTrying again... (attempt $i of 5)"
else
#apt may have succeeded or failed, but exit retry loop for Other Apt Update
break
fi
done
local errors="$(echo "$output" | grep '^[(E)|(Err]:')"
if [ -z "$errors" ] && [ "$exitcode" != 0 ]; then
echo -e "\e[91mFailed to install the packages!\e[39m"
echo "User error: Apt exited with a failed exitcode ($exitcode) and no error (E/Err) output. This could indicate system corruption (eg: storage corruption or unstable overclocking)."
exit 1
elif [ ! -z "$errors" ];then
echo -e "\e[91mFailed to install the packages!\e[39m"
echo -e "The APT reported these errors:\n\e[91m$errors\e[39m"
#some error reports seem to indicate that package URLs aren't being properly downloaded. This output aims to solve the mystery.
if [ "$using_local_packages" == 1 ] && [ ! -f /tmp/pi-apps-local-packages/Packages ];then
echo "User error: Uh-oh, the /tmp/pi-apps-local-packages folder went missing while installing packages.\nThis usually happens if you try to install several apps at the same time in multiple terminals."
elif [ "$using_local_packages" == 1 ] && ( echo "$output" | grep -q 'but it is not installable' || echo "$output" | grep -q 'but it is not going to be installed' || echo "$output" | grep -q "but .* is to be installed" ) ;then
echo -e "\e[91mThe Pi-Apps Local Repository was being used, and a package seemed to not be available. Here's the Packages file:\e[39m"
cat /tmp/pi-apps-local-packages/Packages
echo -e "Attempting apt --dry-run installation of the problematic package(s) for debugging purposes:\n"
( echo "$output" | grep 'but it is not installable' || echo "$output" | grep 'but it is not going to be installed' || echo "$output" | grep "but .* is to be installed" ) | awk '{print $4}' | uniq | xargs sudo -E apt-get install -fy --no-install-recommends --allow-downgrades --dry-run "${apt_flags[@]}"
echo -e "Printing apt-cache policy output for debugging purposes:\n"
apt-cache policy
fi
exit 1
fi
fi
rm -f /tmp/$package_name.deb
rm -rf /tmp/$package_name
#delete the local repository if it was used
if [ "$using_local_packages" == 1 ];then
repo_rm
fi
status_green "Package installation complete."
}
purge_packages() { #Allow dependencies of the $app app to be autoremoved.
#the $app variable must contain something
[ -z "$app" ] && error 'purge_packages function can only be used by apps to install packages. (the $app variable was not set)'
status "Allowing packages required by the $app app to be uninstalled"
#to avoid issues with symbols and spaces in app names, we shasum the app name for use in apt
local package_name="$(app_to_pkgname "$app")"
#if dummy deb found/installed
if package_installed "$package_name" ;then
echo "These packages were: $(package_dependencies "$package_name")"
status "Purging the $package_name package..."
if [ "$script_input" == "update" ]; then
# skip --autoremove for faster updates. this prevents dummy deb dependencies from needing to be uninstalled and reinstalled on updates
apt_lock_wait
local output="$(sudo -E apt purge -y "$package_name" 2>&1 | less_apt | tee /dev/stderr)"
status "Apt finished."
else
apt_lock_wait
local output="$(sudo -E apt purge -y "$package_name" --autoremove 2>&1 | less_apt | tee /dev/stderr)"
status "Apt finished."
fi
errors="$(echo "$output" | grep '^[(E)|(Err]:')"
if [ ! -z "$errors" ];then
echo -e "\e[91mFailed to uninstall the packages!\e[39m"
echo -e "APT reported these errors:\n\e[91m$errors\e[39m"
#run some apt error diagnosis
echo "$output"
exit 1
fi
elif [ -f "${DIRECTORY}/data/installed-packages/${app}" ];then
#legacy pkg-install implementation
warning "Using the old implementation - an installed-packages file instead of a dummy deb"
local packages="$(cat "${DIRECTORY}/data/installed-packages/${app}" | tr '\n' ' ' | sed 's/ / /g')"
#normal mode
local output="$(sudo -E apt purge -y $packages 2>&1)"
exitcode=$?
errors="$(echo "$output" | grep '^[(E)|(Err]:')"
if [ $exitcode != 0 ] || [ ! -z "$errors" ];then
echo -e "\e[91mFailed to uninstall the packages!\e[39m"
echo -e "APT reported these errors:\n\e[91m$errors\e[39m"
#run some apt error diagnosis
echo "$output"
exit 1
fi
else
status "The $package_name package is not installed so there's nothing to do."
fi
status_green "All packages have been purged successfully."
rm -f "${DIRECTORY}/data/installed-packages/${app}"
}
get_icon_from_package() { #given a package-name, find all png files that it installed and print the one with the largest file-size.
[ -z "$1" ] && error "get_icon_from_package(): requires an apt package name"
#Find dependencies of the listed packages and scan them too
local package=''
local extra_packages=''
for package in "$@" ;do
#for every package specified, look for dependencies to that package that begin with the same name as the original package
#Example: given the 'shotwell' package, this will find the 'shotwell-common' package, as well as others
extra_packages+=" $(package_dependencies "$package" | sed 's/, \||/\n/g' | awk '{print $1}' | grep "^$package" | sort | uniq | tr '\n' ' ')"
done
dpkg-query -L "$@" $extra_packages 2>/dev/null | grep '\.png$\|\.svg$' | grep '/icons/\|/pixmaps/' | grep -v /symbolic/ | xargs wc -c | grep -v ' total' | sort -nr | head -n1 | sed 's/ / /g' | sed 's/^ //g' | tr ' ' '\n' | tail -n +2
}
ubuntu_ppa_installer() { #setup a PPA on an Ubuntu distro. Arguments: ppa_name
local ppa_name="$1"
[ -z "$1" ] && error "ubuntu_ppa_installer(): This function is used to add a ppa to a ubuntu based install but a required input argument was missing."
local ppa_grep="$ppa_name"
[[ "${ppa_name}" != */ ]] && local ppa_grep="${ppa_name}/"
local ppa_added=$(apt-get indextargets --no-release-info --format '$(SITE) $(RELEASE) $(TARGET_OF)' | sort -u | awk '{if ($3=="deb") print $1" "$2 }' | grep "$ppa_grep" | wc -l)
if [[ $ppa_added -eq "1" ]]; then
status "Skipping $ppa_name PPA, already added"
else
status "Adding $ppa_name PPA"
sudo add-apt-repository "ppa:$ppa_name" -y || exit 1
apt_update || exit 1
fi
# check if ppa .list filename does not exist under the current distro codename
# on a distro upgrade the .list filename is not updated and add-apt-repository can re-use the old filename
local ppa_dist="$__os_codename"
local standard_filename="/etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-${ppa_dist}.list"
if [[ ! -f "$standard_filename" ]] && ls /etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-*.list 1> /dev/null; then
local original_filename="$(ls /etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-*.list | head -1)"
# change the filename to match the current distro codename
sudo mv "$original_filename" "$standard_filename"
sudo rm -f "$original_filename".distUpgrade
sudo rm -f "$original_filename".save
fi
}
debian_ppa_installer() { #setup a PPA on a Debian distro. Arguments: ppa_name distribution key
local ppa_name="$1"
local ppa_dist="$2"
local key="$3"
[ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] && error "debian_ppa_installer(): This function is used to add a ppa to a debian based install but a required input argument was missing."
local ppa_grep="$ppa_name"
[[ "${ppa_name}" != */ ]] && local ppa_grep="${ppa_name}/ubuntu ${ppa_dist}"
local ppa_added=$(apt-get indextargets --no-release-info --format '$(SITE) $(RELEASE) $(TARGET_OF)' | sort -u | awk '{if ($3=="deb") print $1" "$2 }' | grep "$ppa_grep" | wc -l)
if [[ $ppa_added -eq "1" ]]; then
status "Skipping $ppa_name PPA, already added"
else
status "Adding $ppa_name PPA"
echo "deb https://ppa.launchpadcontent.net/${ppa_name}/ubuntu ${ppa_dist} main" | sudo tee /etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-${ppa_dist}.list || error "Failed to add repository to sources.list!"
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys "$key"
if [ $? != 0 ];then
sudo rm -f /etc/apt/sources.list.d/${ppa_name%/*}-ubuntu-${ppa_name#*/}-${ppa_dist}.list
error "Failed to sign the $ppa_name PPA!"
fi
apt_update || exit 1
fi
}
add_external_repo() { # add an external apt repo and its gpg key.
# follows https://wiki.debian.org/DebianRepository/UseThirdParty specification with deb822 format https://repolib.readthedocs.io/en/latest/deb822-format.html
# required inputs
local reponame="$1"
local pubkeyurl="$2"
local uris="$3"
local suites="$4"
# potentially optional inputs
# components is not used when suite is an absolute path
local components="$5"
# additional options can be specified as the 6th, 7th, 8th, etc argument (eg: "Architectures: arm64")
# check if all needed vars are set
[ -z "$reponame" ] && error "add_external_repo: reponame not set"
[ -z "$uris" ] && error "add_external_repo: uris not set"
[ -z "$suites" ] && error "add_external_repo: suites not set"
[ -z "$pubkeyurl" ] && error "add_external_repo: pubkeyurl not set"
# exit if reponame or uri or suite contains space
if [[ $reponame = *" "* ]] || [[ $uris = *" "* ]] || [[ $suites = *" "* ]]; then
error "add_external_repo: provided reponame contains a space."
fi
# check if links are valid
status "add_external_repo: checking 3rd party pubkeyurl validity"
wget --spider "$pubkeyurl" || error "add_external_repo: pubkeyurl isn't a valid link"
# make apt keyring directory if it doesn't exist
if [ ! -d /usr/share/keyrings ]; then
sudo mkdir -p /usr/share/keyrings || error "add_external_repo: failed to create apt keyring directory."
fi
# check if .list file already exists
if [ -f /etc/apt/sources.list.d/${reponame}.list ]; then
sudo rm -f /etc/apt/sources.list.d/${reponame}.list || error "add_external_repo: failed to remove conflicting .list file."
fi
# check if .sources file already exists
if [ -f /etc/apt/sources.list.d/${reponame}.sources ]; then
sudo rm -f /etc/apt/sources.list.d/${reponame}.sources || error "add_external_repo: failed to remove conflicting .sources file."
fi
# download gpg key from specified url
if [ -f /usr/share/keyrings/${reponame}-archive-keyring.gpg ]; then
sudo rm -f /usr/share/keyrings/${reponame}-archive-keyring.gpg
fi
wget -qO- "$pubkeyurl" | sudo gpg --dearmor -o /usr/share/keyrings/${reponame}-archive-keyring.gpg
if [ $? != 0 ];then
sudo rm -f /etc/apt/sources.list.d/${reponame}.sources
sudo rm -f /usr/share/keyrings/${reponame}-archive-keyring.gpg
error "add_external_repo: download from specified pubkeyurl failed."
fi
# create .sources file
echo "Types: deb
URIs: $uris
Suites: $suites" | sudo tee /etc/apt/sources.list.d/${reponame}.sources >/dev/null
if [ ! -z "$components" ]; then
echo "Components: $components" | sudo tee -a /etc/apt/sources.list.d/${reponame}.sources >/dev/null
fi
for input in "${@: 6}"; do
echo "$input" | sudo tee -a /etc/apt/sources.list.d/${reponame}.sources >/dev/null
done
echo "Signed-By: /usr/share/keyrings/${reponame}-archive-keyring.gpg" | sudo tee -a /etc/apt/sources.list.d/${reponame}.sources >/dev/null
if [ $? != 0 ];then
sudo rm -f /etc/apt/sources.list.d/${reponame}.sources
sudo rm -f /usr/share/keyrings/${reponame}-archive-keyring.gpg
error "add_external_repo: failed to create ${reponame}.list file"
fi
}
rm_external_repo() { # remove an external apt repo and its gpg key, if the repo is no longer in use. (force-remove the repo with force argument)
local reponame="$1"
local force="$2"
[ -z "$reponame" ] && error "rm_external_repo: reponame not provided"
# exit if reponame contains space, since apt doesn't accept .list files with spaces in filename or keyname.
if [[ $reponame = *" "* ]]; then
error "rm_external_repo: provided reponame contains a space."
fi
# always remove deprecated .list file if present
if [ -f /etc/apt/sources.list.d/${reponame}.list ]; then
sudo rm -f /etc/apt/sources.list.d/${reponame}.list
fi
# exit gracefully if .sources file does not exist
[ -f "/etc/apt/sources.list.d/$reponame.sources" ] || return 0
if [ "$force" == force ]; then
sudo rm -f /usr/share/keyrings/${reponame}-archive-keyring.gpg || error "rm_external_repo: removal of ${reponame}-archive-keyring.gpg failed"
sudo rm -f /etc/apt/sources.list.d/${reponame}.sources || error "rm_external_repo: removal of ${reponame}.sources failed"
return 0
fi
remove_repofile_if_unused /etc/apt/sources.list.d/${reponame}.sources "" /usr/share/keyrings/${reponame}-archive-keyring.gpg
}
adoptium_installer() {
case "$__os_codename" in
bionic | focal | jammy | noble | buster | bullseye | bookworm)
add_external_repo "adoptium" "https://adoptium.jfrog.io/artifactory/api/security/keypair/default-gpg-key/public" "https://adoptium.jfrog.io/artifactory/deb" "$__os_codename" "main" || exit 1
;;
*)
# use bionic target name explicitly for any other OS as its the oldest LTS target adoptium continues to support
# all supported adoptium OSs use the same debs so the target specified does not actually matter
add_external_repo "adoptium" "https://adoptium.jfrog.io/artifactory/api/security/keypair/default-gpg-key/public" "https://adoptium.jfrog.io/artifactory/deb" "bionic" "main" || exit 1
;;
esac
apt_update
if [ $? != 0 ]; then
rm_external_repo "adoptium"
error "Failed to perform apt update after adding Adoptium repository."
fi
}
pipx_install() {
# install pipx keeping in mind distro issues
# pipx < 0.16.0 is compatible with 3.6 <= python3 < 3.9
# 0.16.0 <= pipx < 1.1.0 is compatible with python3 >= 3.6
# 1.1.0 <= pipx < 1.3.0 is compatible with python3 >= 3.7
# pipx >= 1.3.0 is compatible with python3 >= 3.8
# some distros lack pipx entirely
# some distros (raspbian bullseye specifically) have incompatible combinations of pipx (0.12.3) and python3 (3.9) versions, necessitating pipx to be installed/upgraded from pip
# pipx 1.0.0 is the first stable release and has some features that we would like to assume are available, install it from pip if the distro package is too old
# pi-apps scripts and functions should assume that ONLY a minimum pipx version of 1.0.0 is available
if package_available pipx && package_is_new_enough pipx 1.0.0 ;then
install_packages pipx python3-venv || exit 1
elif package_is_new_enough python3 3.7 ; then
install_packages python3-venv || exit 1
sudo -H python3 -m pip install --upgrade pipx || exit 1
elif package_available python3.8 ;then
install_packages python3.8 python3.8-venv || exit 1
sudo -H python3.8 -m pip install --upgrade pipx || exit 1
else
error "pipx is not available on your distro and so cannot install $* to python venv"
fi
sudo PIPX_HOME=/usr/local/pipx PIPX_BIN_DIR=/usr/local/bin pipx install "$@" || error "Failed to install $* with pipx"
}
pipx_uninstall() {
sudo PIPX_HOME=/usr/local/pipx PIPX_BIN_DIR=/usr/local/bin pipx uninstall "$@" || error "Failed to uninstall $* with pipx"
}
remove_deprecated_app() { # prompts a user to uninstall a deprecated pi-apps application and then removes the application folder if it exists
local app="$1" # on app name