Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·1211 lines (1132 loc) · 55 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·1211 lines (1132 loc) · 55 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
#!/bin/sh
# Pi-Bolt installer.
#
# curl -fsSL https://pi-bolt.opensec.in/install.sh | sh
#
# Downloads a release, verifies its SHA-256 checksum, installs it to ~/.pi-bolt and links `pi-bolt` into ~/.local/bin. Run
# it again to reinstall, update or uninstall. In Termux (Android) it also installs Termux's glibc, which runs Pi-Bolt, and links
# `pi-bolt` into $PREFIX/bin. The executable comes from the npm registry (a CDN, fast in most places) and
# falls back to GitHub; the checksums always come from the GitHub release, so a download from npm is checked against them.
#
# Environment:
# PIBOLT_VERSION a release tag such as bolt-v0.2.0 (default: the latest release)
# PIBOLT_CHANNEL stable (the latest release; the default) or beta (the newest release, pre-releases included), for this
# installation and its updates (`pi-bolt update --channel` changes it)
# PIBOLT_VARIANT on Linux x86-64, x64, x64-baseline or x64-jit (default: x64 on CPUs with AVX2, x64-baseline otherwise);
# on Linux ARM64 and in Termux, arm64 or arm64-jit (default arm64); on macOS arm64 or arm64-jit (Apple
# silicon; default arm64)
# PIBOLT_INSTALL where to install (default: ~/.pi-bolt)
# PIBOLT_BIN_DIR where to link the `pi-bolt` command (default: ~/.local/bin; in Termux $PREFIX/bin)
# PIBOLT_YES=1 do not ask: take the default action and do not offer to start Pi-Bolt
# PIBOLT_LAUNCHER=1 used by the npm package's first run: download only, no menu, link or prompts
# PIBOLT_CONNECTIONS connections to download over at once (default: 4; 1 for a single connection)
# PIBOLT_SOURCE where to download the executable from: auto (npm, then GitHub; the default), npm or github
# PIBOLT_NPM_REGISTRY the npm registry or mirror to use (default: https://registry.npmjs.org)
# PIBOLT_UNVERIFIED the SHA-256 of a release's SHA256SUMS (the installer shows it), to install that download without a
# check of its signature: where nothing here can check it (no OpenSSL 3, OpenSSH 8.1 or Pi-Bolt installed
# before), or for a release from before 0.6.0, which has none. It matches those checksums only, so a value
# left in the environment does nothing for another download; the npm package's first run ignores it. The
# checksum is still checked, and a signature that does not verify always stops the install.
# PIBOLT_DOWNLOAD_BASE a mirror of a release to download everything from (its signatures too); needs PIBOLT_VERSION when
# which release is the latest cannot be found out
# PIBOLT_EXTENSIONS yes or no: whether to install OpenSec's optional extensions (opensec-pi-subagents, opensec-pi-todo, opensec-pi-fff)
# without asking. Without it the installer asks when it can; a run that cannot ask, or PIBOLT_YES=1,
# installs none. They are installed at the versions the release pins (its extensions.txt, covered by the
# signed checksums), and only if the registry's tarball has the pinned integrity.
# The public key that releases are signed with (keys/release.pub in the repository; scripts/sign-release.sh), from 0.6.0. A
# release from before has no signature: it is installed only with PIBOLT_UNVERIFIED.
RELEASE_KEY="-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAoLboJqtKaoISPqffk03vHZr+1sRBG3uIRIWeKOew+aY=
-----END PUBLIC KEY-----"
ESC=$(printf '\033')
CR=$(printf '\r')
ETX=$(printf '\003')
REPO="https://github.com/opensec-git/Pi-Bolt"
API="https://api.github.com/repos/opensec-git/Pi-Bolt"
NPM_REGISTRY="${PIBOLT_NPM_REGISTRY:-https://registry.npmjs.org}"
main() {
set -eu
if [ "$(uname -s)" = Darwin ]; then PLATFORM=darwin; else PLATFORM=linux; fi
VERSION="${PIBOLT_VERSION:-latest}"
INSTALL="${PIBOLT_INSTALL:-$HOME/.pi-bolt}"
TERMUX=""
if is_termux; then TERMUX=1; fi
if [ -n "$TERMUX" ]; then BIN_DIR="${PIBOLT_BIN_DIR:-$PREFIX/bin}"; else BIN_DIR="${PIBOLT_BIN_DIR:-$HOME/.local/bin}"; fi
LAUNCHER="${PIBOLT_LAUNCHER:-0}"
setup_style
# (The npm package's first run is not asked for by the user: nothing is installed there without a signature check.)
[ "$LAUNCHER" != 1 ] || unset PIBOLT_UNVERIFIED
case "$VERSION" in
latest) ;;
bolt-v[0-9]*.[0-9]*.[0-9]*) case "${VERSION#bolt-v}" in *[!0-9.]*) fail "PIBOLT_VERSION must be a release tag such as bolt-v1.0.3" ;; esac ;;
*) fail "PIBOLT_VERSION must be a release tag such as bolt-v1.0.3" ;;
esac
case "${PIBOLT_CHANNEL:-}" in
"" | stable) ;;
beta)
# (The newest release is looked up once, and everything is downloaded from that tag.)
if [ "$VERSION" = latest ] && [ -z "${PIBOLT_DOWNLOAD_BASE:-}" ]; then
tag=$(newest_release) && [ -n "$tag" ] || fail "could not find the newest release ($API/releases): try again, or set PIBOLT_VERSION"
VERSION="bolt-v$tag"
fi
;;
*) fail "PIBOLT_CHANNEL must be stable or beta" ;;
esac
check_file="$(mktemp)"
preflight >"$check_file" 2>&1 &
check_pid=$!
version_file="$(mktemp)"
resolve_version >"$version_file" 2>/dev/null &
version_pid=$!
logo_animation
if wait "$check_pid"; then check_status=0; else check_status=$?; fi
if [ "$LAUNCHER" = 1 ]; then
printf '%s Pi-Bolt%s\n%s First run: getting the native executable%s\n\n' "$bold" "$reset" "$dim" "$reset"
else
printf '%s Pi-Bolt Installer%s\n%s Pi, compiled ahead of time. Native speed, no JIT.%s\n\n' "$bold" "$reset" "$dim" "$reset"
fi
cat "$check_file"
rm -f "$check_file"
[ "$check_status" -eq 0 ] || exit "$check_status"
wait "$version_pid" 2>/dev/null || true
SHOWN_VERSION=$(cat "$version_file")
rm -f "$version_file"
[ -n "$SHOWN_VERSION" ] || SHOWN_VERSION="$VERSION"
# A mirror's checksums are taken for the release asked for only: without knowing which one that is, an older release (signed
# all the same) could be served as the latest.
if [ -n "${PIBOLT_DOWNLOAD_BASE:-}" ]; then
case "$SHOWN_VERSION" in
[0-9]*.[0-9]*.[0-9]*) ;;
*) fail "which release is the latest could not be found out ($REPO/releases/latest), and a mirror (PIBOLT_DOWNLOAD_BASE) is taken for the release asked for only: set PIBOLT_VERSION to the release it serves, such as bolt-v1.0.3" ;;
esac
fi
VARIANT="${PIBOLT_VARIANT:-}"
if [ -z "$VARIANT" ]; then
if [ "$PLATFORM" = darwin ] || is_arm64; then VARIANT=arm64
elif grep -qw avx2 /proc/cpuinfo 2>/dev/null; then VARIANT=x64; else VARIANT=x64-baseline; fi
fi
case "$PLATFORM-$VARIANT" in
darwin-arm64 | darwin-arm64-jit) ;;
darwin-*) fail "PIBOLT_VARIANT must be arm64 or arm64-jit on macOS" ;;
linux-arm64 | linux-arm64-jit) is_arm64 || fail "PIBOLT_VARIANT must be x64, x64-baseline or x64-jit on x86-64" ;;
linux-x64 | linux-x64-baseline | linux-x64-jit) ! is_arm64 || fail "PIBOLT_VARIANT must be arm64 or arm64-jit on ARM64" ;;
*) if is_arm64; then fail "PIBOLT_VARIANT must be arm64 or arm64-jit on ARM64"; else fail "PIBOLT_VARIANT must be x64, x64-baseline or x64-jit"; fi ;;
esac
[ -z "$TERMUX" ] || setup_termux
case "${PIBOLT_SOURCE:-auto}" in
auto | npm | github) ;;
*) fail "PIBOLT_SOURCE must be auto, npm or github" ;;
esac
NAME="pi-bolt-$PLATFORM-$VARIANT"
if [ "$VERSION" = latest ]; then BASE="$REPO/releases/latest/download"; else BASE="$REPO/releases/download/$VERSION"; fi
BASE="${PIBOLT_DOWNLOAD_BASE:-$BASE}"
if [ "$LAUNCHER" = 1 ]; then
install_release
return 0
fi
EXISTING=""
if [ -x "$INSTALL/$NAME/pi" ]; then EXISTING="$INSTALL/$NAME"; fi
choose_action
case "$ACTION" in
none) exit 0 ;;
uninstall)
uninstall
printf '\nPi-Bolt was uninstalled.\n'
exit 0
;;
esac
install_release
# The channel this installation's updates take (Pi-Bolt reads <install>/channel; stable, the default, is no file).
case "${PIBOLT_CHANNEL:-}" in
beta) printf 'beta\n' >"$INSTALL/channel" ;;
stable) rm -f "$INSTALL/channel" ;;
esac
mkdir -p "$BIN_DIR"
ln -sfn "$INSTALL/$NAME/$(command_of_build)" "$BIN_DIR/pi-bolt"
# Pi-Bolt keeps the packages only it runs (OpenSec's extensions) out of Pi's list, which it does as it starts: once now, so
# that Pi, if it is started before Pi-Bolt, does not find them (an earlier installer put them there).
(cd "$INSTALL" && "$INSTALL/$NAME/pi" list >/dev/null 2>&1) || true
if [ "$ACTION" = reinstall ] && [ -n "${installed:-}" ] && [ "${installed:-}" != "$SHOWN_VERSION" ]; then word=updated
elif [ "$ACTION" = reinstall ]; then word=reinstalled
else word=installed; fi
printf '\nPi-Bolt %s was %s successfully %s(Pi %s)%s.\n' "$SHOWN_VERSION" "$word" "$dim" "$("$INSTALL/$NAME/pi" --version)" "$reset"
# `pi-bolt compile` compiled extensions into a build here: this one, the release's, has none (they load from their files).
if [ -f "$INSTALL/compile.json" ]; then
printf 'Extensions were compiled into the build this replaced: run %spi-bolt compile%s to compile them into this one.\n' "$bold" "$reset"
fi
offer_extensions
if [ "$(command -v pi-bolt 2>/dev/null || true)" = "$BIN_DIR/pi-bolt" ]; then
printf '\nRun it with: %spi-bolt%s\n' "$bold" "$reset"
else
path_hint
fi
offer_start
}
# --- Look -------------------------------------------------------------------------------------------------------------
setup_style() {
reset="" dim="" bold="" cyan="" green="" red=""
amber="" amber2="" blue="" blue2="" white=""
FULL="#" EMPTY="-" FRAMES=4 CHECK="ok" EIGHTHS=0
if [ -t 1 ] && [ "${TERM:-}" != dumb ]; then
reset="${ESC}[0m" dim="${ESC}[2m" bold="${ESC}[1m" cyan="${ESC}[36m" green="${ESC}[32m" red="${ESC}[31m"
amber="${ESC}[38;2;247;192;74m" amber2="${ESC}[38;2;233;164;44m"
blue="${ESC}[38;2;76;150;234m" blue2="${ESC}[38;2;42;120;214m" white="${ESC}[38;2;255;255;255m"
if unicode_terminal; then FULL="█" EMPTY="░" FRAMES=10 CHECK="✓" EIGHTHS=1; fi
fi
}
unicode_terminal() {
case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
*UTF-8* | *utf-8* | *UTF8* | *utf8*) return 0 ;;
esac
case "${TERM_PROGRAM:-}" in
Apple_Terminal | iTerm.app | vscode | WezTerm | ghostty) return 0 ;;
esac
return 1
}
# The bolt, on a grid of 8 x 9 cells (each cell two characters wide). Amber is the upper stroke and the left half of the
# step, blue the right half and the lower stroke; the last cell of each row is a shade darker, like the logo's facets.
UPPER="0,6 0,7 1,5 1,6 2,4 2,5 3,3 3,4 4,2 4,3 4,4"
LOWER="4,5 4,6 4,7 5,5 5,6 6,4 6,5 7,3 7,4 8,3"
UPPER_DARK="0,7 1,6 2,5 3,4"
LOWER_DARK="4,7 5,6 6,5 7,4"
has_cell() { # has_cell Y X "cells" [DY]: is (Y - DY, X) one of the cells
want="$(($1 - ${4:-0})),$2"
for c in $3; do [ "$c" = "$want" ] && return 0; done
return 1
}
# draw_bolt UPPER_DY LOWER_DY MODE: MODE is color, white or none (nothing of that piece).
draw_bolt() {
frame=""
y=0
while [ "$y" -le 8 ]; do
frame="$frame "
x=1
while [ "$x" -le 8 ]; do
cell=" "
if [ "$1" != off ] && has_cell "$y" "$x" "$UPPER" "$1"; then
if [ "$3" = white ]; then cell="${white}██"
elif has_cell "$y" "$x" "$UPPER_DARK" "$1"; then cell="${amber2}██"
else cell="${amber}██"; fi
elif [ "$2" != off ] && has_cell "$y" "$x" "$LOWER" "$2"; then
if [ "$3" = white ]; then cell="${white}██"
elif has_cell "$y" "$x" "$LOWER_DARK" "$2"; then cell="${blue2}██"
else cell="${blue}██"; fi
fi
frame="$frame$cell$reset"
x=$((x + 1))
done
frame="$frame
"
y=$((y + 1))
done
printf '%s' "$frame"
}
logo_animation() {
if [ ! -t 1 ] || [ "${TERM:-}" = dumb ]; then
printf '\n'
draw_bolt 0 0 color | sed 's/██/##/g'
printf '\n'
return
fi
printf '%s[?25l\n' "$ESC"
trap 'printf "%s%s[?25h\n" "$reset" "$ESC"; exit 130' INT TERM
up="${ESC}[9A"
first=1
show() { if [ "$first" = 1 ]; then first=0; else printf '%s' "$up"; fi; draw_bolt "$@"; sleep "$SPEED"; }
SPEED=0.06
for dy in -6 -4 -2 -1 0; do show "$dy" off color; done
for dy in 6 4 2 1 0; do show 0 "$dy" color; done
SPEED=0.07
show 0 0 white
show 0 0 color
show 0 0 white
SPEED=0.35
show 0 0 color
printf '%s[?25h\n' "$ESC"
trap - INT TERM
}
spinner() {
if [ "$FRAMES" -eq 10 ]; then
case $(($1 % 10)) in
0) printf '⠋' ;; 1) printf '⠙' ;; 2) printf '⠹' ;; 3) printf '⠸' ;; 4) printf '⠼' ;;
5) printf '⠴' ;; 6) printf '⠦' ;; 7) printf '⠧' ;; 8) printf '⠇' ;; *) printf '⠏' ;;
esac
else
case $(($1 % 4)) in 0) printf '-' ;; 1) printf '%s' "\\" ;; 2) printf '|' ;; *) printf '/' ;; esac
fi
}
# draw_progress STEP FRACTION LABEL: FRACTION is 0-10000 (hundredths of a percent), or -1 when the total is not known (a
# moving comet instead). The bar fills in eighths of a cell, so that it moves however slow the download is.
BAR_WIDTH=28
draw_progress() {
bar=""
i=0
if [ "$2" -ge 0 ]; then
eighths=$(($2 * BAR_WIDTH * 8 / 10000))
while [ "$i" -lt "$BAR_WIDTH" ]; do
if [ "$i" -lt $((BAR_WIDTH / 2)) ]; then color=$amber; else color=$blue; fi
left=$((eighths - i * 8))
if [ "$left" -ge 8 ]; then
bar="$bar$color$FULL"
elif [ "$left" -gt 0 ] && [ "$EIGHTHS" = 1 ]; then
bar="$bar$color$(eighth "$left")"
else
bar="$bar$dim$EMPTY"
fi
bar="$bar$reset"
i=$((i + 1))
done
else
head=$(($1 % (BAR_WIDTH + 6)))
while [ "$i" -lt "$BAR_WIDTH" ]; do
age=$((head - i))
if [ "$age" -ge 0 ] && [ "$age" -lt 2 ]; then bar="$bar$white$FULL"
elif [ "$age" -ge 2 ] && [ "$age" -lt 4 ]; then bar="$bar$amber$FULL"
elif [ "$age" -ge 4 ] && [ "$age" -lt 6 ]; then bar="$bar$blue$FULL"
else bar="$bar$dim$EMPTY"; fi
bar="$bar$reset"
i=$((i + 1))
done
fi
printf '\r%s[K %s%s%s %s %sInstalling Pi-Bolt%s %s' "$ESC" "$amber" "$(spinner "$1")" "$reset" "$bar" "$bold" "$reset" "$3"
}
# A cell filled N eighths from the left.
eighth() {
case $1 in 1) printf '▏' ;; 2) printf '▎' ;; 3) printf '▍' ;; 4) printf '▌' ;; 5) printf '▋' ;; 6) printf '▊' ;; *) printf '▉' ;; esac
}
finish_progress() { printf '\r%s[K' "$ESC"; [ -t 1 ] && printf '%s[?25h' "$ESC"; return 0; }
# shellcheck disable=SC2088 # a literal ~ for display
tilde() { case "$1" in "$HOME"/*) printf '~/%s' "${1#"$HOME"/}" ;; *) printf '%s' "$1" ;; esac; }
# The release tag the latest download points to, as a version: bolt-v0.2.0 -> 0.2.0.
resolve_version() {
if [ "$VERSION" != latest ]; then printf '%s' "${VERSION#bolt-v}"; return; fi
command -v curl >/dev/null 2>&1 || return 0
net_curl -fsSIL -o /dev/null -w '%{url_effective}' "$REPO/releases/latest" | sed -n 's#.*/tag/bolt-v##p'
}
# The newest release, pre-releases included (the beta channel), as a version: the highest bolt-vX.Y.Z tag GitHub lists (it
# lists no drafts to anyone without write access).
newest_release() {
net_curl -fsSL -H 'Accept: application/vnd.github+json' "$API/releases?per_page=30" |
sed -n 's/^ *"tag_name": *"bolt-v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' |
sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1
}
mb() { awk -v b="$1" 'BEGIN { printf "%.1f", b / 1048576 }'; }
# --- Steps ------------------------------------------------------------------------------------------------------------
fail() {
finish_progress
printf '%serror:%s %s\n' "$red" "$reset" "$*" >&2
exit 1
}
preflight() {
status=0
case "$(uname -s)" in
Linux) preflight_linux ;;
Darwin) preflight_macos ;;
*) printf 'error: Pi-Bolt runs on Linux (x86-64, ARM64), Android (Termux) and macOS (Apple silicon) only (this is %s).\n' "$(uname -s)"; status=1 ;;
esac
command -v tar >/dev/null 2>&1 || { printf 'error: tar is required.\n'; status=1; }
if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then
printf 'error: sha256sum or shasum is required.\n'
status=1
fi
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
printf 'error: curl or wget is required.\n'
status=1
fi
[ "$status" -eq 0 ] || printf '\n'
return "$status"
}
preflight_macos() {
# An ARM64 Mac, also from a shell that runs under Rosetta (the executable runs natively all the same).
if [ "$(uname -m)" != arm64 ] && [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" != 1 ]; then
printf 'error: Pi-Bolt runs on Macs with Apple silicon (M1 or later) only; this Mac has an Intel CPU.\n'
status=1
fi
macos=$(sw_vers -productVersion 2>/dev/null || echo 0)
if [ "${macos%%.*}" -lt 13 ] 2>/dev/null; then
printf 'error: Pi-Bolt needs macOS 13 (Ventura) or later (this Mac has %s).\n' "$macos"
status=1
fi
}
preflight_linux() {
case "$(uname -m)" in
x86_64 | aarch64 | arm64) ;;
*) printf 'error: Pi-Bolt runs on x86-64 and ARM64 only (this is %s).\n' "$(uname -m)"; status=1 ;;
esac
# (Termux: Android's libc is not glibc. setup_termux() installs Termux's glibc, which runs Pi-Bolt.)
if [ -n "$TERMUX" ]; then
is_arm64 || { printf 'error: in Termux, Pi-Bolt runs on 64-bit ARM only (this is %s).\n' "$(uname -m)"; status=1; }
command -v pkg >/dev/null 2>&1 || [ -e "$PREFIX/glibc/lib/ld-linux-aarch64.so.1" ] ||
{ printf "error: Termux's glibc is needed, and there is no pkg to install it with.\n"; status=1; }
return 0
fi
if ldd --version 2>&1 | grep -qi musl; then
printf 'error: Pi-Bolt needs glibc; musl-based systems such as Alpine are not supported.\n'
status=1
fi
glibc=$(ldd --version 2>/dev/null | sed -n '1s/.* \([0-9][0-9]*\)\.\([0-9][0-9]*\)$/\1 \2/p')
if [ -n "$glibc" ] && [ "$(echo "$glibc" | awk '{ print ($1 < 2 || ($1 == 2 && $2 < 17)) }')" = 1 ]; then
printf 'error: Pi-Bolt needs glibc 2.17 or later (this system has %s).\n' "$(echo "$glibc" | tr ' ' .)"
status=1
fi
# Every x86-64 build needs SSE4.2 (the runtime itself does): without it the executable stops with "Illegal instruction".
if [ -r /proc/cpuinfo ] && grep -q '^flags' /proc/cpuinfo && ! grep -qw sse4_2 /proc/cpuinfo; then
printf 'error: Pi-Bolt needs a CPU with SSE4.2: Intel Nehalem (2008) or later, AMD Bulldozer (2011) or later.\n'
status=1
fi
}
# check_sum FILE: whether FILE (in the current folder) matches its line in SHA256SUMS. (shasum where there is no sha256sum: macOS
# before 15.)
# A file that SHA256SUMS has no line for does not match (macOS's sha256sum passes when it is given nothing to check).
check_sum() {
line=$(grep " $1\$" SHA256SUMS) && [ -n "$line" ] || return 1
if command -v sha256sum >/dev/null 2>&1; then
printf '%s\n' "$line" | sha256sum -c --quiet - >/dev/null 2>&1
else
printf '%s\n' "$line" | shasum -a 256 -c --status - 2>/dev/null
fi
}
# curl that gives up on a connection after a few seconds: one of the addresses a name has can stop answering (seen with
# GitHub's release servers), and curl then waits for the system's timeout, 75 s on macOS, before trying the next. With a
# timeout, it tries the next address after half of it.
net_curl() { curl --connect-timeout 8 "$@"; }
fetch() { # fetch FILE URL
if command -v curl >/dev/null 2>&1; then net_curl -fsSL --retry 2 -o "$1" "$2"; else wget -q -O "$1" "$2"; fi
}
# probe URL: prints "FINAL SIZE RANGES": where the download ends up after redirects ("-" if not known), its size in bytes (0 if
# not known), and 1 if that server sends parts of it (Range requests).
probe() {
if command -v curl >/dev/null 2>&1; then
net_curl -fsSIL -w 'url %{url_effective}\n' "$1" 2>/dev/null | tr -d '\r' | awk '
/^HTTP\// { n = 0; r = 0 }
tolower($1) == "content-length:" { n = $2 }
tolower($1) == "accept-ranges:" { r = (tolower($2) == "bytes") }
$1 == "url" { u = $2 }
END { print (u == "" ? "-" : u), n + 0, r + 0 }'
else
wget -q --spider -S "$1" 2>&1 | tr -d '\r' | awk 'tolower($1) == "content-length:" { n = $2 } END { print "-", n + 0, 0 }'
fi
}
# npm_probe URL: like probe, for a file on the npm registry, which sends no size for a HEAD request: asks for its first byte
# and reads the size from the reply. Fails if the registry does not have it.
npm_probe() {
command -v curl >/dev/null 2>&1 || return 1
net_curl -fsSL -r 0-0 -D - -o /dev/null -w 'url %{url_effective}\n' "$1" 2>/dev/null | tr -d '\r' | awk '
tolower($1) == "content-range:" { split($3, a, "/"); n = a[2] }
$1 == "url" { u = $2 }
END { if (u == "" || n + 0 == 0) exit 1; print u, n + 0, 1 }'
}
# What to download, worked out in the background while the bar moves: the checksums, then the smaller .tar.xz if the release
# has one and xz is installed (otherwise .tar.gz), where from and how big it is. The .tar.xz is also published on npm, as the
# package pi-bolt-PLATFORM-VARIANT (pi-bolt-linux-x64, pi-bolt-darwin-arm64, ...) of the same version: that is tried first. Writes "SOURCE EXT FINAL SIZE RANGES" to $TMP/plan.
plan_download() {
if ! fetch "$TMP/SHA256SUMS" "$BASE/SHA256SUMS" 2>/dev/null; then
echo fail >"$TMP/plan"
return
fi
ext=tar.gz
if can_unxz && grep -q " $NAME.tar.xz\$" "$TMP/SHA256SUMS"; then ext=tar.xz; fi
where=""
if [ "$ext" = tar.xz ] && [ "${PIBOLT_SOURCE:-auto}" != github ] && [ -z "${PIBOLT_DOWNLOAD_BASE:-}" ]; then
case "$SHOWN_VERSION" in
[0-9]*.[0-9]*.[0-9]*) where=$(npm_probe "$(npm_url)") && where="npm $where" || where="" ;;
esac
fi
if [ -z "$where" ] && [ "${PIBOLT_SOURCE:-auto}" = npm ]; then
echo fail-npm >"$TMP/plan"
return
fi
[ -n "$where" ] || where="github $(probe "$BASE/$NAME.$ext")"
printf '%s\n' "${where%% *} $ext ${where#* }" >"$TMP/plan.part" && mv "$TMP/plan.part" "$TMP/plan"
}
# Whether a .tar.xz can be unpacked: with xz, or with a tar that reads it itself (bsdtar with liblzma: macOS's tar).
can_unxz() { command -v xz >/dev/null 2>&1 || tar --version 2>/dev/null | grep -q liblzma; }
# The npm package that holds this release's .tar.xz.
npm_url() { printf '%s/%s/-/%s-%s.tgz' "$NPM_REGISTRY" "$NAME" "$NAME" "$SHOWN_VERSION"; }
# start_download URL FINAL SIZE RANGES: starts fetching into $TMP/part.N in the background, the process IDs in $PIDS. A big file
# from a server that sends parts comes in PIBOLT_CONNECTIONS parts at once (default 4): over a long distance, one connection
# is limited by the round trip and several are faster.
start_download() {
PIDS=""
rm -f "$TMP"/part.*
connections=${PIBOLT_CONNECTIONS:-4}
if command -v curl >/dev/null 2>&1 && [ "$4" = 1 ] && [ "$2" != - ] && [ "$3" -ge 16777216 ] && [ "$connections" -gt 1 ]; then
chunk=$((($3 + connections - 1) / connections))
n=0
while [ "$n" -lt "$connections" ]; do
from=$((n * chunk))
to=$((from + chunk - 1))
[ "$to" -lt "$3" ] || to=$(($3 - 1))
net_curl -fsS --retry 2 -r "$from-$to" -o "$TMP/part.$n" "$2" 2>>"$TMP/fetch.log" &
PIDS="$PIDS $!"
n=$((n + 1))
done
else
fetch "$TMP/part.0" "$1" 2>>"$TMP/fetch.log" &
PIDS=$!
fi
}
running() {
for pid in $PIDS; do kill -0 "$pid" 2>/dev/null && return 0; done
return 1
}
all_succeeded() { # true if every process in $PIDS succeeded
ok=0
for pid in $PIDS; do wait "$pid" || ok=1; done
return "$ok"
}
# shellcheck disable=SC2012 # the installer's own file names
received() { ls -ln "$TMP"/part.* 2>/dev/null | awk '{ n += $5 } END { print n + 0 }'; }
# Hundredths of a second since boot (Linux), or since the epoch (elsewhere: what counts is the difference).
centiseconds() {
awk '{ printf "%d", $1 * 100 }' /proc/uptime 2>/dev/null || perl -MTime::HiRes=time -e 'printf "%d", time * 100' 2>/dev/null || echo 0
}
# rate_and_eta GOT TOTAL CENTISECONDS: "3.2 MB/s, 12s left", from the average so far.
rate_and_eta() {
[ "$3" -ge 50 ] && [ "$1" -gt 0 ] || return 0
awk -v got="$1" -v total="$2" -v t="$3" 'BEGIN {
rate = got / (t / 100)
printf " %.1f MB/s", rate / 1048576
if (total > got) {
left = int((total - got) / rate + 0.5)
if (left >= 60) printf ", %dm %02ds left", left / 60, left % 60; else printf ", %ds left", left
}
}'
}
# The checksums are signed by Pi-Bolt's release key, from 0.6.0 on. For those releases the signature is required: one that
# cannot be downloaded, or that does not verify, stops the install, whatever the download base. It is checked with OpenSSL 3
# (LibreSSL, macOS's own openssl, cannot check Ed25519), or else with ssh-keygen against SHA256SUMS.sshsig, the same key's
# signature in OpenSSH's format (published for releases after 0.9.9; macOS has OpenSSH), or else with the Pi-Bolt installed
# here before. Where none of them can, or for a release from before 0.6.0 (which has no signature), nothing is installed
# unless PIBOLT_UNVERIFIED is the SHA-256 of these checksums (unverified_allowed): the checksums come from the same place as
# the files, so they alone do not show that a release is Pi-Bolt's. Both signatures are fetched in the background while the
# release downloads.
fetch_signature() {
if fetch "$TMP/SHA256SUMS.sig.part" "$BASE/SHA256SUMS.sig" 2>/dev/null; then mv "$TMP/SHA256SUMS.sig.part" "$TMP/SHA256SUMS.sig"; fi
if fetch "$TMP/SHA256SUMS.sshsig.part" "$BASE/SHA256SUMS.sshsig" 2>/dev/null; then mv "$TMP/SHA256SUMS.sshsig.part" "$TMP/SHA256SUMS.sshsig"; fi
return 0
}
# The release key in OpenSSH's form, for SHA256SUMS.sshsig (node scripts/lib/sshsig.mjs pubkey keys/release.pub).
RELEASE_SSH_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKC26CarSmqCEj6n35NN7x2a/tbEQRt7iESFnijnsPmm"
# Whether ssh-keygen can verify signatures (-Y, OpenSSH 8.1 and later).
ssh_keygen_verifies() {
command -v ssh-keygen >/dev/null 2>&1 || return 1
case "$(ssh-keygen -Y find-principals 2>&1)" in *"illegal option"* | *"unknown option"* | *"invalid option"*) return 1 ;; esac
}
# Whether this release is from after signing began; one whose version is not known counts as one.
signed_release() {
case "$SHOWN_VERSION" in
0.[0-5].*) return 1 ;;
*) return 0 ;;
esac
}
# An OpenSSL 3 or later: the one on PATH, or Homebrew's.
openssl3() {
for candidate in openssl /opt/homebrew/opt/openssl@3/bin/openssl /usr/local/opt/openssl@3/bin/openssl; do
command -v "$candidate" >/dev/null 2>&1 || continue
case "$("$candidate" version 2>/dev/null)" in
"OpenSSL "[3-9]* | "OpenSSL "[1-9][0-9]*)
printf '%s' "$candidate"
return 0
;;
esac
done
return 1
}
# The SHA-256 of FILE, in lower case.
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum <"$1"; else shasum -a 256 <"$1"; fi | cut -c1-64 | tr 'A-F' 'a-f'
}
# A Pi-Bolt executable installed here before, which checks the signature with its own crypto (BUN_BE_BUN=1 runs a script):
# a build in its own folder (not one kept for a rollback, nor one left by an update that stopped).
installed_pibolt() {
for candidate in "$INSTALL/$NAME/pi-bin" "$INSTALL/$NAME/pi" "$INSTALL"/pi-bolt-"$PLATFORM"-*/pi-bin "$INSTALL"/pi-bolt-"$PLATFORM"-*/pi; do
case "${candidate#"$INSTALL"/}" in *.*/*) continue ;; esac
if [ -f "$candidate" ] && [ -x "$candidate" ]; then
printf '%s' "$candidate"
return 0
fi
done
return 1
}
# (It says what it checked: the SHA-256 of the checksums whose signature verified. An exit status alone is not taken for a
# check: a program that does not run the script, or runs it otherwise, can end with 0 too.)
VERIFY_SCRIPT='const c = require("node:crypto"), f = require("node:fs");
let ok, sums;
try { sums = f.readFileSync(process.argv[2]); ok = c.verify(null, sums, f.readFileSync(process.argv[3], "utf8"), f.readFileSync(process.argv[4])); } catch { process.exit(4); }
if (!ok) process.exit(3);
process.stdout.write("verified " + c.createHash("sha256").update(sums).digest("hex") + "\n");'
# Whether the user let this download be installed without a signature check: PIBOLT_UNVERIFIED is the SHA-256 of its
# SHA256SUMS, which no other download has (a value kept in a profile does not let another release in).
unverified_allowed() {
[ -n "${PIBOLT_UNVERIFIED:-}" ] && [ "$(printf '%s' "$PIBOLT_UNVERIFIED" | tr 'A-F' 'a-f')" = "$SUMS_SHA256" ]
}
# fail_unverified WHY: stops, saying how to install this download all the same.
fail_unverified() {
if [ "$LAUNCHER" = 1 ]; then
fail "$1 Nothing was installed. Install Pi-Bolt with its installer instead (curl -fsSL https://pi-bolt.opensec.in/install.sh | sh), which says how to go on."
fi
case "$SHOWN_VERSION" in [0-9]*.[0-9]*.[0-9]*) page="$REPO/releases/tag/bolt-v$SHOWN_VERSION" ;; *) page="$REPO/releases/latest" ;; esac
stale=""
if [ -n "${PIBOLT_UNVERIFIED:-}" ]; then stale=" (PIBOLT_UNVERIFIED is set, but not to this download's SHA-256: it applies to one download only)"; fi
fail "$1 Nothing was installed$stale. To install this download without the check, compare the SHA-256 of its SHA256SUMS, $SUMS_SHA256, with the one GitHub shows for SHA256SUMS on $page from a device you trust, and run the installer again with PIBOLT_UNVERIFIED=$SUMS_SHA256."
}
verify_signature() {
[ -n "$RELEASE_KEY" ] || return 0
# (Whether it worked is whether the file is there: under set -e a failed wait would end the script without a word.)
if [ -n "$SIG_PID" ]; then wait "$SIG_PID" 2>/dev/null || true; fi
SUMS_SHA256=$(sha256_of "$TMP/SHA256SUMS")
bad="the release's signature does not verify: the download is not Pi-Bolt's. Nothing was installed."
if [ ! -f "$TMP/SHA256SUMS.sig" ]; then
signed_release && fail "could not download the release's signature from $BASE/SHA256SUMS.sig. Pi-Bolt $SHOWN_VERSION is signed, so nothing was installed: try again."
unverified_allowed || fail_unverified "Pi-Bolt $SHOWN_VERSION is from before Pi-Bolt signed its releases: nothing shows that this download is Pi-Bolt's."
UNSIGNED=1
return 0
fi
# (An Ed25519 signature is 64 bytes: anything else, such as a page of HTML served in its place, is not one.)
[ "$(wc -c <"$TMP/SHA256SUMS.sig" | tr -d ' ')" = 64 ] || fail "$bad"
printf '%s\n' "$RELEASE_KEY" >"$TMP/release.pub"
if ssl=$(openssl3) && "$ssl" pkey -pubin -in "$TMP/release.pub" -noout >/dev/null 2>&1; then
"$ssl" pkeyutl -verify -pubin -inkey "$TMP/release.pub" -rawin -in "$TMP/SHA256SUMS" -sigfile "$TMP/SHA256SUMS.sig" >/dev/null 2>&1 || fail "$bad"
SIGNED=1
return 0
fi
if [ -f "$TMP/SHA256SUMS.sshsig" ] && ssh_keygen_verifies; then
printf 'pi-bolt namespaces="pi-bolt-release" %s\n' "$RELEASE_SSH_KEY" >"$TMP/allowed_signers"
ssh-keygen -Y verify -f "$TMP/allowed_signers" -I pi-bolt -n pi-bolt-release -s "$TMP/SHA256SUMS.sshsig" <"$TMP/SHA256SUMS" >/dev/null 2>&1 || fail "$bad"
SIGNED=1
return 0
fi
if exe=$(installed_pibolt); then
# (By its full path: it is run from the download's folder, below.)
case "$exe" in /*) ;; *) exe="$PWD/$exe" ;; esac
printf '%s\n' "$VERIFY_SCRIPT" >"$TMP/verify.js"
status=0
# (Run in the download's own folder, which holds only the release's files.)
said=$(cd "$TMP" && BUN_BE_BUN=1 "$exe" ./verify.js SHA256SUMS release.pub SHA256SUMS.sig 2>/dev/null) || status=$?
[ "$status" != 3 ] || fail "$bad"
if [ "$status" = 0 ] && [ "$said" = "verified $SUMS_SHA256" ]; then
SIGNED=1
return 0
fi
fi
if ! unverified_allowed; then
if [ "$(uname -s)" = Darwin ]; then how='OpenSSL 3 (brew install openssl@3)'; else how='OpenSSL 3, or openssh-client (OpenSSH 8.1 or later), from your package manager'; fi
fail_unverified "nothing here can check the release's signature (OpenSSL 3, or OpenSSH 8.1 for releases after 0.9.9). Install $how and run the installer again."
fi
NOVERIFY=1
}
# The checksums' first line says which release they are ("# pi-bolt X.Y.Z"): an older release's, signed all the same and served
# as a newer one's, would otherwise install the older release as if it were the newer. (0.7.1 to 0.7.3 were published without
# that line; every release from 0.8.0 on has it, as install.ps1 requires.)
check_release_version() {
case "$SHOWN_VERSION" in
[0-9]*.[0-9]*.[0-9]*) ;;
*) return 0 ;; # (Which release "latest" is was not found out: nothing to compare with.)
esac
first=$(head -n 1 "$TMP/SHA256SUMS")
case "$first" in
"# pi-bolt $SHOWN_VERSION") return 0 ;;
"# pi-bolt "*) fail "the release's checksums are those of Pi-Bolt ${first#\# pi-bolt }, not $SHOWN_VERSION. Nothing was installed." ;;
esac
major=${SHOWN_VERSION%%.*}
rest=${SHOWN_VERSION#*.}
if [ "$major" -gt 0 ] || [ "${rest%%.*}" -ge 8 ]; then
fail "the release's checksums do not say which version they are. Nothing was installed."
fi
}
# download URL FINAL SIZE RANGES FILE: fetches URL into FILE with the progress bar, in parts if the server allows; false if it
# could not.
download() {
[ -t 1 ] || printf 'downloading %s (%s MB)\n' "$1" "$(mb "$3")"
start_download "$1" "$2" "$3" "$4"
began=$(centiseconds)
while [ -t 1 ] && running; do
got=$(received)
extra=$(rate_and_eta "$got" "$3" $(($(centiseconds) - began)))
if [ "$3" -gt 0 ]; then
fraction=$((got * 10000 / $3))
[ "$fraction" -le 10000 ] || fraction=10000
draw_progress "$step" "$fraction" "${dim}downloading $(mb "$got") / $(mb "$3") MB$extra$reset"
else
draw_progress "$step" -1 "${dim}downloading $(mb "$got") MB$extra$reset"
fi
step=$((step + 1))
sleep 0.08
done
if ! all_succeeded || { [ "$PIDS" != "${PIDS% *}" ] && [ "$(received)" != "$3" ]; }; then
# The parts did not all arrive, or not as asked for (a proxy may send the whole file for each): once more, in one piece.
[ "$PIDS" = "${PIDS% *}" ] && return 1
start_download "$1" - 0 0
while [ -t 1 ] && running; do
draw_progress "$step" -1 "${dim}downloading $(mb "$(received)") MB, again in one piece$reset"
step=$((step + 1))
sleep 0.08
done
all_succeeded || return 1
fi
rm -f "$5"
n=0
while [ -f "$TMP/part.$n" ]; do
cat "$TMP/part.$n" >>"$5"
rm -f "$TMP/part.$n"
n=$((n + 1))
done
total=$(wc -c <"$5" | tr -d ' ')
}
install_release() {
SIGNED="" UNSIGNED="" NOVERIFY=""
# The download is kept next to where it is installed: a /tmp mounted noexec could not run the check of the executable
# below, and the last step is then a rename on the same file system.
mkdir -p "$INSTALL" 2>/dev/null || true
TMP="$(mktemp -d "$INSTALL/.download.XXXXXX" 2>/dev/null || mktemp -d)"
PIDS=""
SIG_PID=""
# (Whatever ends the script, a failed step too, takes the download with it.)
trap 'kill $PIDS $SIG_PID 2>/dev/null || true; rm -rf "$TMP"' EXIT
trap 'kill $PIDS $SIG_PID 2>/dev/null || true; rm -rf "$TMP"; finish_progress; exit 130' INT TERM
[ -t 1 ] && printf '%s[?25l' "$ESC"
if [ ! -t 1 ]; then
# Not a terminal (a log, CI): plain lines instead of an animated bar.
draw_progress() { printf '%s\n' "$3" | sed "s/${ESC}\\[[0-9;]*m//g"; }
finish_progress() { :; }
fi
step=0
plan_download &
plan_pid=$!
if [ -n "$RELEASE_KEY" ]; then
fetch_signature &
SIG_PID=$!
fi
while [ -t 1 ] && [ ! -f "$TMP/plan" ] && kill -0 "$plan_pid" 2>/dev/null; do
draw_progress "$step" -1 "${dim}connecting$reset"
step=$((step + 1))
sleep 0.08
done
wait "$plan_pid" 2>/dev/null
read -r source ext final total ranges <"$TMP/plan" 2>/dev/null || source=fail
[ "$source" != fail ] || fail "download failed: $BASE/SHA256SUMS"
[ "$source" != fail-npm ] || fail "npm does not have Pi-Bolt $SHOWN_VERSION: $(npm_url)"
if [ "$source" = npm ]; then
# The package is a .tgz with the release's .tar.xz in it, checked against the release's checksums like any download.
if download "$final" "$final" "$total" "$ranges" "$TMP/npm.tgz" &&
tar -C "$TMP" -xzf "$TMP/npm.tgz" "package/$NAME.$ext" 2>/dev/null &&
mv "$TMP/package/$NAME.$ext" "$TMP/$NAME.$ext" &&
{ draw_progress "$step" 10000 "${dim}verifying checksum$reset"; (cd "$TMP" && check_sum "$NAME.$ext"); }; then
FROM=npm
else
[ "${PIBOLT_SOURCE:-auto}" != npm ] || fail "download failed: $final"
rm -rf "$TMP/npm.tgz" "$TMP/package" "$TMP/$NAME.$ext"
finish_progress
printf ' %snot available from npm; downloading from GitHub instead%s\n' "$dim" "$reset"
source=github
# shellcheck disable=SC2046 # three words
set -- $(probe "$BASE/$NAME.$ext")
final=$1 total=$2 ranges=$3
fi
fi
if [ "$source" = github ]; then
download "$BASE/$NAME.$ext" "$final" "$total" "$ranges" "$TMP/$NAME.$ext" || fail "download failed: $BASE/$NAME.$ext"
FROM=GitHub
[ -z "${PIBOLT_DOWNLOAD_BASE:-}" ] || FROM="$PIBOLT_DOWNLOAD_BASE"
draw_progress "$step" 10000 "${dim}verifying checksum$reset"
(cd "$TMP" && check_sum "$NAME.$ext") ||
fail "checksum mismatch: the download is corrupt or incomplete"
fi
if [ -n "$SIG_PID" ] && kill -0 "$SIG_PID" 2>/dev/null; then
draw_progress "$step" 10000 "${dim}fetching the signature$reset"
fi
verify_signature
check_release_version
# The optional extensions this release pins (extensions.txt, covered by the checksums and their signature).
PINS=""
if fetch "$TMP/extensions.txt" "$BASE/extensions.txt" 2>/dev/null && (cd "$TMP" && check_sum extensions.txt); then
PINS=$(grep -E '^[a-z0-9][a-z0-9._-]* [0-9]+\.[0-9]+\.[0-9]+ sha512-[A-Za-z0-9+/]+=*$' "$TMP/extensions.txt" || true)
fi
if [ "$ext" = tar.xz ]; then
if command -v xz >/dev/null 2>&1; then
(xz -T0 -dc "$TMP/$NAME.$ext" 2>/dev/null || xz -dc "$TMP/$NAME.$ext") | tar -C "$TMP" -xf - &
else
tar -C "$TMP" -xf "$TMP/$NAME.$ext" &
fi
else
tar -C "$TMP" -xzf "$TMP/$NAME.$ext" &
fi
PIDS=$!
draw_progress "$step" 10000 "${dim}extracting$reset"
while [ -t 1 ] && running; do
step=$((step + 1))
draw_progress "$step" 10000 "${dim}extracting$reset"
sleep 0.08
done
all_succeeded || fail "could not extract $NAME.$ext"
draw_progress "$((step + 2))" 10000 "${dim}checking the executable$reset"
if [ -n "$TERMUX" ]; then
set_interpreter "$TMP/$NAME/pi" "$LOADER" || fail "could not set the executable's loader to $LOADER"
write_termux_launcher "$TMP/$NAME/pi-termux" "$INSTALL/$NAME"
fi
"$TMP/$NAME/pi" --version >/dev/null 2>&1 || fail "the downloaded executable does not run on this system"
mkdir -p "$INSTALL"
rm -rf "$INSTALL/$NAME.old"
# The build this replaces is kept as $NAME.previous, for `pi-bolt update --rollback`; the same build installed again
# (the same pi-bolt.txt) is not, and the one kept before stays.
if [ -d "$INSTALL/$NAME" ]; then
if [ "$LAUNCHER" != 1 ] && ! cmp -s "$INSTALL/$NAME/pi-bolt.txt" "$TMP/$NAME/pi-bolt.txt"; then
rm -rf "$INSTALL/$NAME.previous"
mv "$INSTALL/$NAME" "$INSTALL/$NAME.previous"
else
mv "$INSTALL/$NAME" "$INSTALL/$NAME.old"
fi
fi
mv "$TMP/$NAME" "$INSTALL/$NAME"
rm -rf "$INSTALL/$NAME.old" "$TMP"
trap - EXIT INT TERM
finish_progress
printf ' %s%s%s install complete %s(%s, %s MB from %s%s)%s\n' "$green" "$CHECK" "$reset" "$dim" "$PLATFORM-$VARIANT" "$(mb "${total:-0}")" "$FROM" "${SIGNED:+, signature verified}" "$reset"
if [ -n "$UNSIGNED$NOVERIFY" ]; then
if [ -n "$UNSIGNED" ]; then why="this release has no signature (it is from before Pi-Bolt signed releases)"; else why="the release's signature was not checked (nothing here can)"; fi
printf ' %s%sWARNING:%s %s. It was installed because PIBOLT_UNVERIFIED is the SHA-256 of its SHA256SUMS: that checksum is all that shows what it is.\n' "$bold" "$red" "$reset" "$why" >&2
fi
}
uninstall() {
# (The builds kept for a rollback too: pi-bolt-<platform>-<variant>.previous.)
for dir in "$INSTALL"/pi-bolt-linux-* "$INSTALL"/pi-bolt-darwin-*; do
[ -d "$dir" ] && rm -rf "$dir"
done
# (And what `pi-bolt compile` keeps or leaves here: compile.log from a failed build, and the work folder of one killed.)
rm -rf "$INSTALL/cache" "$INSTALL/compile.json" "$INSTALL/compile.log" "$INSTALL/channel" "$INSTALL"/.compile-*
target=$(readlink "$BIN_DIR/pi-bolt" 2>/dev/null || true)
case "$target" in
"$INSTALL"/*) rm -f "$BIN_DIR/pi-bolt" ;;
esac
rmdir "$INSTALL" 2>/dev/null || true
}
# --- Questions --------------------------------------------------------------------------------------------------------
has_tty() { [ "${PIBOLT_YES:-0}" != 1 ] && (: <>/dev/tty) 2>/dev/null; }
read_key() {
old=$(stty -g </dev/tty 2>/dev/null || true)
stty -icanon -echo min 1 time 0 </dev/tty 2>/dev/null || true
key=$(dd bs=1 count=1 2>/dev/null </dev/tty || true)
[ -n "$old" ] && stty "$old" </dev/tty 2>/dev/null
printf '%s' "$key"
}
# drop_typeahead: drops what was typed at the terminal before a question. read_key takes one key, so the Enter typed after
# it stays in the terminal's input, and would answer the next question with its default (#103).
drop_typeahead() {
old=$(stty -g </dev/tty 2>/dev/null || true)
if [ -n "$old" ] && stty -icanon min 0 time 0 </dev/tty 2>/dev/null; then
# (With min 0 and time 0 a read returns at once, with nothing once the input is empty.)
drained=0
while [ "$drained" -lt 64 ] && [ "$(dd bs=256 count=1 2>/dev/null </dev/tty | wc -c | tr -d ' ')" != 0 ]; do
drained=$((drained + 1))
done
stty "$old" </dev/tty 2>/dev/null || true
fi
}
# ask FORMAT [ARG...]: asks a question (printf arguments) and sets `answer` to the line typed after it. What was typed before
# is dropped before the question is shown, so an answer typed as soon as it appears is kept.
ask() {
drop_typeahead
# shellcheck disable=SC2059
printf "$@"
answer=$(head -n 1 </dev/tty || true)
}
choose_action() {
if [ -n "$EXISTING" ]; then default=reinstall; else default=install; fi
if [ -n "$EXISTING" ]; then
installed=""
[ -f "$EXISTING/pi-bolt.txt" ] && installed=$(sed -n 's/^Pi-Bolt \([0-9.]*\) .*/\1/p' "$EXISTING/pi-bolt.txt")
if [ -n "$installed" ] && [ "$installed" != "$SHOWN_VERSION" ]; then
printf '%sPi-Bolt %s is installed at:%s\n\n %s\n\n' "$bold" "$installed" "$reset" "$(tilde "$EXISTING")"
else
printf '%sPi-Bolt is already installed at:%s\n\n %s\n\n' "$bold" "$reset" "$(tilde "$EXISTING")"
fi
fi
case "$VARIANT" in
x64) cpu="for CPUs with AVX2" ;;
x64-baseline) cpu="for any x86-64 CPU" ;;
arm64) if [ "$PLATFORM" = darwin ]; then cpu="for Apple silicon"; else cpu="for any ARM64 CPU"; fi ;;
*) cpu="with the JIT on" ;;
esac
printf '%sInstallation:%s\n\n' "$bold" "$reset"
printf ' %sPi-Bolt %s%s, %s-%s build %s\n' "$amber" "$SHOWN_VERSION" "$reset" "$PLATFORM" "$VARIANT" "$cpu"
printf ' %sinstalls to%s %s\n' "$dim" "$reset" "$(tilde "$INSTALL/$NAME")"
printf ' %scommand%s %s\n\n' "$dim" "$reset" "$(tilde "$BIN_DIR/pi-bolt")"
printf '%sChoose an action:%s\n\n' "$bold" "$reset"
if [ -n "$EXISTING" ]; then
if [ -n "$installed" ] && [ "$installed" != "$SHOWN_VERSION" ]; then action="Update to Pi-Bolt $SHOWN_VERSION"; else action="Reinstall Pi-Bolt"; fi
printf ' %s%-4s%s %s%s%s %s(default)%s\n' "$cyan" y "$reset" "$green" "$action" "$reset" "$dim" "$reset"
printf ' %s%-4s%s %sUninstall Pi-Bolt%s\n' "$cyan" u "$reset" "$red" "$reset"
else
printf ' %s%-4s%s %sInstall Pi-Bolt%s %s(default)%s\n' "$cyan" y "$reset" "$green" "$reset" "$dim" "$reset"
fi
printf ' %s%-4s%s %sDo nothing%s\n' "$cyan" n "$reset" "$dim" "$reset"
if ! has_tty; then
ACTION=$default
else
while :; do
key=$(read_key)
case "$key" in
"" | " " | "$CR" | y | Y) ACTION=$default; break ;;
u | U) if [ -n "$EXISTING" ]; then ACTION=uninstall; break; fi ;;
n | N | "$ESC") ACTION=none; break ;;
"$ETX") exit 130 ;;
esac
printf 'Please choose one of the listed keys.\n'
done
fi
case "$ACTION" in
install) printf '\nWill install Pi-Bolt.\n\n' ;;
reinstall) printf '\nWill reinstall Pi-Bolt.\n\n' ;;
uninstall) printf '\nWill uninstall Pi-Bolt.\n' ;;
none) printf '\nChose to do nothing. Exiting.\n' ;;
esac
}
shell_config_file() {
case "$(basename "${SHELL:-sh}")" in
zsh) printf '%s/.zshrc' "${ZDOTDIR:-$HOME}" ;;
fish) printf '%s/.config/fish/config.fish' "$HOME" ;;
bash)
# A macOS terminal starts a login shell, which reads the first of these and not ~/.bashrc.
if [ "$PLATFORM" != darwin ] && [ -f "$HOME/.bashrc" ]; then printf '%s/.bashrc' "$HOME"
elif [ "$PLATFORM" = darwin ] && [ -f "$HOME/.bash_profile" ]; then printf '%s/.bash_profile' "$HOME"
elif [ "$PLATFORM" = darwin ] && [ -f "$HOME/.bash_login" ]; then printf '%s/.bash_login' "$HOME"
else printf '%s/.profile' "$HOME"; fi
;;
*) printf '%s/.profile' "$HOME" ;;
esac
}
path_hint() {
# (A folder of the user's own choosing goes into a line their shell runs: quoted, so that it stays a folder.)
if [ "$BIN_DIR" = "$HOME/.local/bin" ]; then expr="\$HOME/.local/bin"; else expr=$(printf '%s' "$BIN_DIR" | sed 's/[\\"$`]/\\&/g'); fi
if [ "$(basename "${SHELL:-sh}")" = fish ]; then line="fish_add_path \"$expr\""; else line="export PATH=\"$expr:\$PATH\""; fi
config=$(shell_config_file)
printf '\n%s is not on your PATH yet.\n' "$(tilde "$BIN_DIR")"
if [ -f "$config" ] && grep -Fxq "$line" "$config"; then
printf 'Your %s already adds it: restart your shell, then run pi-bolt.\n' "$(tilde "$config")"
return 0
fi
if has_tty; then
ask 'Add it to your PATH in %s now? [Y/n] ' "$(tilde "$config")"
case "$answer" in
n | N | no | NO) ;;
*)
mkdir -p "$(dirname "$config")"
printf '\n# Pi-Bolt\n%s\n' "$line" >>"$config"
printf 'Added it to %s. Restart your shell, or run:\n\n %s\n' "$(tilde "$config")" "$line"
return 0
;;
esac
fi
printf 'Add this to %s, then restart your shell:\n\n %s\n' "$(tilde "$config")" "$line"
}
# OpenSec's optional extensions: Bun builds that load fast in Pi-Bolt (see their READMEs on npm). Pi-Bolt installs them
# itself, with npm where it is installed and otherwise with the package manager built into Pi-Bolt: nothing else is needed.
# The last field matches other extensions of the same kind (an extended regular expression on `pi-bolt list`): one of those
# registers the same tools, so Pi would refuse to load both. A local checkout counts too, by its folder's name.
EXTENSIONS="opensec-pi-subagents:run specialized agents in separate sessions:subagents
opensec-pi-todo:a todo list for the model, shown above the editor:rpiv-todo|pi-todo
opensec-pi-fff:fast file and content search (find, grep, @-mentions):pi-fff"
# pin_of PACKAGE: the release's line for it in extensions.txt ("NAME VERSION sha512-..."); false if there is none.
pin_of() { printf '%s\n' "${PINS:-}" | grep -m 1 "^$1 "; }
# pinned_tarball_matches PACKAGE VERSION INTEGRITY: whether the registry's tarball of that version has that integrity (SHA-512),