-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·1273 lines (1172 loc) · 61.2 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·1273 lines (1172 loc) · 61.2 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
#!/usr/bin/env bash
# VoidLinux-Guide interactive installer.
# Run from an official Void Linux live image. Read tools/README.md and run --dry-run first.
set -Eeuo pipefail
IFS=$'\n\t'
INSTALLER_VERSION="P1-2026.08"
SCRIPT_NAME="${0##*/}"
REPO_BASE="https://repo-default.voidlinux.org"
OFFICIAL_MIRROR_HOSTS=(repo-default.voidlinux.org repo-de.voidlinux.org repo-fi.voidlinux.org repo-fr.voidlinux.org)
REPO_URL=""
TARGET_ROOT="/mnt/voidlinux-guide"
TARGET_DISK=""
TARGET_ARCH="auto"
TARGET_LIBC="glibc"
BOOT_MODE="auto"
PARTITION_LAYOUT="single-root"
ROOT_SIZE_GIB="32"
SWAP_SIZE_GIB="4"
ESP_SIZE_MIB="512"
BOOT_SIZE_GIB="2"
TARGET_MIRROR_URL=""
DRY_RUN=0
NON_INTERACTIVE=0
CONFIG_FILE=""
CONFIG_LOADED=0
RESUME_FILE=""
RECOVERY_FILE=""
RESUMING=0
RECOVERING=0
CONFIRM_TOKEN=""
STATE_FILE=""
COMPLETED_STEPS=""
ENCRYPTION="none"
CRYPT_CIPHER="aes-xts-plain64"
CRYPT_KEY_SIZE="512"
CRYPT_PASSPHRASE=""
VG_NAME="voidvm"
ROOT_LV_NAME="root"
HOME_LV_NAME="home"
SWAP_LV_NAME="swap"
ROOT_CRYPT_NAME="voidroot"
HOME_CRYPT_NAME="voidhome"
HOME_CRYPT_PASSPHRASE=""
TARGET_USER="voiduser"
ROOT_PASSWORD=""
USER_PASSWORD=""
GENERATE_ROOT_PROFILE=1
GENERATE_USER_PROFILE=1
SHOW_QR=0
DESKTOP="none"
DISPLAY_PROTOCOL="auto"
SESSION_MANAGER="auto"
GPU="auto"
NETWORK_MANAGER=0
FIREWALL="none"
APPARMOR=0
ENABLE_SSH=0
TIMEZONE="UTC"
LOCALE="en_US.UTF-8"
HOSTNAME_VALUE="void"
ESP_DEV=""
DATA_DEV=""
ROOT_DEV=""
HOME_DEV=""
SWAP_DEV=""
CRYPT_UUID=""
HOME_CRYPT_UUID=""
BOOT_DEV=""
ROOT_DATA_DEV=""
HOME_DATA_DEV=""
BACKUP_DIR=""
TPM2_MODE="off"
FIDO2_MODE="off"
SECURE_BOOT_MODE="off"
RED="\033[31m"; YELLOW="\033[33m"; BLUE="\033[34m"; RESET="\033[0m"
usage() {
cat <<USAGE
Usage: $SCRIPT_NAME [OPTIONS]
Interactive installer for native Void Linux targets. Supported targets:
x86_64 glibc, x86_64 musl, aarch64 glibc, aarch64 musl.
Options:
--dry-run Render the plan without modifying disks, packages, services or secrets.
--config FILE Load a reviewed shell-style configuration file.
--resume FILE Resume an incomplete installation from a private state file.
--recover FILE Mount a recorded target and write a non-destructive recovery report.
--non-interactive Require config/resume values and CONFIRM_TOKEN=VOID_INSTALL for real execution.
--target-root DIR Staging root, default: /mnt/voidlinux-guide.
--repo URL Official Void repository URL; normally derived from target arch/libc.
--mirror URL Official mirror base or repository URL; validated before bootstrap.
-h, --help Show this help.
The script uses the official XBPS bootstrap method. A real run can erase an entire
whole-disk target. Run --dry-run, inspect the disk report, and review the final
plan before executing on a disposable or confirmed target disk.
USAGE
}
log() { printf '%b\n' "${BLUE}==>${RESET} $*"; }
warn() { printf '%b\n' "${YELLOW}Warning:${RESET} $*" >&2; }
die() { printf '%b\n' "${RED}Error:${RESET} $*" >&2; exit 1; }
run() {
if (( DRY_RUN )); then
printf '%b' "${YELLOW}[dry-run]${RESET}"
printf ' %q' "$@"
printf '\n'
else
"$@"
fi
}
run_shell() {
if (( DRY_RUN )); then
printf '%b\n' "${YELLOW}[dry-run]${RESET} $*"
else
bash -c "$*"
fi
}
require_command() { command -v "$1" >/dev/null 2>&1 || die "Required command is missing: $1"; }
parse_args() {
while (($#)); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--config) (($# >= 2)) || die "--config requires a file"; CONFIG_FILE="$2"; shift ;;
--resume) (($# >= 2)) || die "--resume requires a state file"; RESUME_FILE="$2"; shift ;;
--recover) (($# >= 2)) || die "--recover requires a state file"; RECOVERY_FILE="$2"; shift ;;
--non-interactive) NON_INTERACTIVE=1 ;;
--target-root) (($# >= 2)) || die "--target-root requires a directory"; TARGET_ROOT="$2"; shift ;;
--repo) (($# >= 2)) || die "--repo requires a URL"; REPO_URL="$2"; shift ;;
--mirror) (($# >= 2)) || die "--mirror requires a URL"; TARGET_MIRROR_URL="$2"; shift ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown option: $1" ;;
esac
shift
done
[[ -z "$CONFIG_FILE" || -z "$RESUME_FILE" ]] || die "Use either --config or --resume, not both"
[[ -z "$RECOVERY_FILE" || ( -z "$CONFIG_FILE" && -z "$RESUME_FILE" ) ]] || die "--recover cannot be combined with --config or --resume"
}
load_config() {
[[ -n "$CONFIG_FILE" ]] || return 0
[[ -r "$CONFIG_FILE" && -f "$CONFIG_FILE" ]] || die "Cannot read configuration file: $CONFIG_FILE"
# A config is intentionally shell syntax and must be reviewed by the operator.
# shellcheck disable=SC1090
source "$CONFIG_FILE"
CONFIG_LOADED=1
}
state_variables() {
cat <<'VARS'
TARGET_ROOT TARGET_DISK TARGET_ARCH TARGET_LIBC BOOT_MODE PARTITION_LAYOUT ROOT_SIZE_GIB SWAP_SIZE_GIB ESP_SIZE_MIB BOOT_SIZE_GIB REPO_URL TARGET_MIRROR_URL ENCRYPTION CRYPT_CIPHER CRYPT_KEY_SIZE VG_NAME ROOT_LV_NAME HOME_LV_NAME SWAP_LV_NAME ROOT_CRYPT_NAME HOME_CRYPT_NAME TARGET_USER GENERATE_ROOT_PROFILE GENERATE_USER_PROFILE SHOW_QR DESKTOP DISPLAY_PROTOCOL SESSION_MANAGER GPU NETWORK_MANAGER FIREWALL APPARMOR ENABLE_SSH TIMEZONE LOCALE HOSTNAME_VALUE ESP_DEV BOOT_DEV DATA_DEV ROOT_DATA_DEV HOME_DATA_DEV ROOT_DEV HOME_DEV SWAP_DEV CRYPT_UUID HOME_CRYPT_UUID BACKUP_DIR TPM2_MODE FIDO2_MODE SECURE_BOOT_MODE COMPLETED_STEPS STATE_FILE
VARS
}
state_file_default() {
if [[ -n "$STATE_FILE" ]]; then return 0; fi
STATE_FILE="/run/voidlinux-guide-installer/$(basename "$TARGET_DISK").state"
}
save_state() {
if (( DRY_RUN )); then
printf '%b\n' "${YELLOW}[dry-run]${RESET} save state $STATE_FILE"
return 0
fi
state_file_default
local directory temporary variable
directory="$(dirname "$STATE_FILE")"
mkdir -p "$directory"
chmod 0700 "$directory"
temporary="${STATE_FILE}.tmp.$$"
umask 077
{
printf '# VoidLinux-Guide private resumable state. No passwords or LUKS passphrase.\n'
printf 'STATE_INSTALLER_VERSION=%q\n' "$INSTALLER_VERSION"
while read -r variable; do
[[ -n "$variable" ]] || continue
printf '%s=%q\n' "$variable" "${!variable}"
done < <(state_variables | tr ' ' '\n')
} > "$temporary"
chmod 0600 "$temporary"
mv -f "$temporary" "$STATE_FILE"
}
validate_state_file() {
[[ -f "$STATE_FILE" && -r "$STATE_FILE" ]] || die "Cannot read resume state: $STATE_FILE"
[[ "$(stat -c '%u' "$STATE_FILE")" == "0" ]] || die "Resume state must be owned by root"
[[ "$(stat -c '%a' "$STATE_FILE")" == "600" ]] || die "Resume state must have mode 0600"
}
load_state() {
[[ -n "$RESUME_FILE" ]] || return 0
STATE_FILE="$RESUME_FILE"
validate_state_file
local runtime_version="$INSTALLER_VERSION"
# State files are created by save_state and use shell-escaped assignments.
# shellcheck disable=SC1090
source "$STATE_FILE"
[[ "${STATE_INSTALLER_VERSION:-}" == "$runtime_version" ]] || die "State file was created by a different installer version"
[[ -n "$TARGET_DISK" && -n "$COMPLETED_STEPS" ]] || die "Incomplete or invalid state file"
RESUMING=1
log "Resuming target $TARGET_DISK from $STATE_FILE"
}
load_recovery_state() {
[[ -n "$RECOVERY_FILE" ]] || return 0
STATE_FILE="$RECOVERY_FILE"
validate_state_file
local runtime_version="$INSTALLER_VERSION"
# State files are generated by this installer and contain shell-escaped, non-secret values.
# shellcheck disable=SC1090
source "$STATE_FILE"
[[ "${STATE_INSTALLER_VERSION:-}" == "$runtime_version" ]] || die "Recovery state was created by a different installer version"
[[ -n "$TARGET_DISK" && -n "$COMPLETED_STEPS" ]] || die "Incomplete or invalid recovery state file"
RECOVERING=1
log "Preparing non-destructive recovery for $TARGET_DISK from $STATE_FILE"
}
step_done() { [[ ",$COMPLETED_STEPS," == *",$1,"* ]]; }
mark_step() {
local step="$1"
step_done "$step" && return
if [[ -z "$COMPLETED_STEPS" ]]; then COMPLETED_STEPS="$step"; else COMPLETED_STEPS+=",$step"; fi
save_state
}
run_stage() {
local step="$1" function="$2"
if step_done "$step"; then
log "Skipping completed stage: $step"
return
fi
log "Running stage: $step"
"$function"
mark_step "$step"
}
check_host() {
if (( DRY_RUN )); then return 0; fi
(( EUID == 0 )) || die "Run as root from an official Void live image. Use --dry-run for safe planning."
[[ -r /etc/os-release ]] || die "Cannot identify host OS"
# shellcheck disable=SC1091
source /etc/os-release
[[ "${ID:-}" == "void" ]] || die "Real installation requires a Void Linux host; detected: ${ID:-unknown}"
}
check_dependencies() {
local commands=(awk basename blkid chmod cp cut date df findmnt grep head ln lsblk mkdir mount mv pgrep printf rm sed sha256sum sort stat sync tee tr umount)
if (( ! DRY_RUN )); then
commands+=(xbps-install sfdisk partprobe mkfs.ext4 mkfs.vfat mkswap swapon swapoff cryptsetup pvcreate vgcreate vgchange lvcreate grub-install grub-mkconfig chroot wipefs)
fi
local command
for command in "${commands[@]}"; do require_command "$command"; done
}
ask() {
local prompt="$1" default="${2:-}"
(( ! NON_INTERACTIVE )) || die "Missing non-interactive value for: $prompt"
if [[ -n "$default" ]]; then read -r -p "$prompt [$default]: " REPLY; REPLY="${REPLY:-$default}"; else read -r -p "$prompt: " REPLY; fi
printf '%s' "$REPLY"
}
choose() {
local prompt="$1"; shift
local options=("$@") answer i
printf '\n%s\n' "$prompt" >&2
for i in "${!options[@]}"; do printf ' %d) %s\n' "$((i+1))" "${options[$i]}" >&2; done
(( ! NON_INTERACTIVE )) || die "Choice required in non-interactive mode: $prompt"
while true; do
read -r -p "Select [1-${#options[@]}]: " answer
[[ "$answer" =~ ^[0-9]+$ ]] && (( answer >= 1 && answer <= ${#options[@]} )) && { printf '%s' "${options[$((answer-1))]}"; return; }
warn "Select a number from 1 to ${#options[@]}."
done
}
confirm() {
local prompt="$1" answer target_name
if (( NON_INTERACTIVE )); then [[ "$CONFIRM_TOKEN" == "VOID_INSTALL" ]] || die "Non-interactive real execution requires CONFIRM_TOKEN=VOID_INSTALL"; return; fi
read -r -p "$prompt Type YES to continue: " answer
[[ "$answer" == "YES" ]] || die "Confirmation not received"
target_name="$(basename "$TARGET_DISK")"
read -r -p "Type target disk basename '$target_name' to confirm: " answer
[[ "$answer" == "$target_name" ]] || die "Target-disk confirmation did not match"
}
random_password() { LC_ALL=C tr -dc 'A-Za-z0-9@%+=_.,:!?-' < /dev/urandom | dd bs=28 count=1 2>/dev/null; }
secure_password() {
local label="$1" secret
(( ! DRY_RUN )) || { printf '%s' '<generated-at-install-time>'; return; }
if [[ "$label" == root && -n "$ROOT_PASSWORD" ]]; then printf '%s' "$ROOT_PASSWORD"; return; fi
if [[ "$label" == user && -n "$USER_PASSWORD" ]]; then printf '%s' "$USER_PASSWORD"; return; fi
secret="$(random_password)"
[[ -n "$secret" ]] || die "Password generation failed"
printf '%s' "$secret"
}
show_qr() {
local secret="$1"
if command -v qrencode >/dev/null 2>&1; then printf '%s' "$secret" | qrencode -t ANSIUTF8
elif [[ -x "$TARGET_ROOT/usr/bin/qrencode" ]]; then printf '%s' "$secret" | chroot "$TARGET_ROOT" qrencode -t ANSIUTF8
else warn "qrencode is unavailable; text output was used instead."; fi
}
show_secret() {
local label="$1" secret="$2"
printf '\n%b\n' "${RED}SECURITY WARNING${RESET}"
printf '%s\n' "The $label password is displayed on this terminal. Cameras, screen sharing and terminal scrollback can capture it. Store it in a password manager and clear terminal scrollback afterwards."
printf '%s\n' "Password: $secret"
if (( SHOW_QR )); then printf '%s\n' "QR payload is the password only. Do not scan it in public."; show_qr "$secret"; fi
(( NON_INTERACTIVE )) || read -r -p "Record the password, then press Enter to continue." _
}
part_path() {
local disk="$1" number="$2"
if [[ "$disk" =~ (nvme|mmcblk)[0-9]+$ ]]; then printf '%sp%s' "$disk" "$number"; else printf '%s%s' "$disk" "$number"; fi
}
is_positive_integer() { [[ "$1" =~ ^[1-9][0-9]*$ ]]; }
normalize_target() {
if [[ "$TARGET_ARCH" == auto ]]; then TARGET_ARCH="$(uname -m)"; fi
[[ "$TARGET_ARCH" == x86_64 || "$TARGET_ARCH" == aarch64 ]] || die "Supported target architectures are x86_64 and aarch64"
[[ "$TARGET_LIBC" == glibc || "$TARGET_LIBC" == musl ]] || die "TARGET_LIBC must be glibc or musl"
if [[ "$TARGET_ARCH" == x86_64 ]]; then
if [[ "$TARGET_LIBC" == glibc ]]; then XBPS_TARGET="x86_64"; else XBPS_TARGET="x86_64-musl"; fi
else
if [[ "$TARGET_LIBC" == glibc ]]; then XBPS_TARGET="aarch64"; else XBPS_TARGET="aarch64-musl"; fi
fi
if [[ -z "$REPO_URL" ]]; then
case "$XBPS_TARGET" in
x86_64) REPO_URL="$REPO_BASE/current" ;;
x86_64-musl) REPO_URL="$REPO_BASE/current/musl" ;;
aarch64|aarch64-musl) REPO_URL="$REPO_BASE/current/aarch64" ;;
esac
fi
if [[ "$BOOT_MODE" == auto ]]; then
if [[ "$TARGET_ARCH" == aarch64 ]]; then BOOT_MODE=uefi
elif [[ -d /sys/firmware/efi ]]; then BOOT_MODE=uefi
else BOOT_MODE=bios; fi
fi
[[ "$BOOT_MODE" == uefi || "$BOOT_MODE" == bios ]] || die "BOOT_MODE must be uefi or bios"
[[ "$TARGET_ARCH" != aarch64 || "$BOOT_MODE" == uefi ]] || die "Generic aarch64 route supports UEFI only"
if (( ! DRY_RUN )) && [[ "$(uname -m)" != "$TARGET_ARCH" ]]; then
die "Cross-architecture chroot is refused. Configure binfmt/QEMU separately for an incompatible host."
fi
normalize_mirror
}
repository_path() {
case "$XBPS_TARGET" in
x86_64) printf 'current' ;;
x86_64-musl) printf 'current/musl' ;;
aarch64|aarch64-musl) printf 'current/aarch64' ;;
*) die "Cannot derive repository path for $XBPS_TARGET" ;;
esac
}
repository_metadata_url() {
printf '%s/%s-repodata' "${REPO_URL%/}" "$XBPS_TARGET"
}
mirror_host() {
sed -E 's#^https://([^/]+).*$#\1#' <<< "$1"
}
is_allowed_mirror_host() {
local host="$1" known
for known in "${OFFICIAL_MIRROR_HOSTS[@]}"; do [[ "$host" == "$known" ]] && return 0; done
return 1
}
normalize_mirror() {
[[ -n "$TARGET_MIRROR_URL" ]] || return 0
local base host path
base="${TARGET_MIRROR_URL%/}"
host="$(mirror_host "$base")"
is_allowed_mirror_host "$host" || die "Mirror host is not in the built-in official allow-list: $host"
path="$(repository_path)"
if [[ "$base" == */current* ]]; then REPO_URL="$base"; else REPO_URL="$base/$path"; fi
}
network_preflight() {
local metadata_url
metadata_url="$(repository_metadata_url)"
log "Network preflight: DNS and HTTPS reachability for $metadata_url"
if (( DRY_RUN )); then
printf '%b\n' "${YELLOW}[dry-run]${RESET} curl --fail --location --head --connect-timeout 10 --max-time 30 $metadata_url"
return 0
fi
require_command curl
curl --fail --location --head --connect-timeout 10 --max-time 30 "$metadata_url" >/dev/null || die "Network preflight failed for repository metadata; check network, DNS, time and mirror URL"
log "Network preflight passed. XBPS signatures remain authoritative for package verification."
}
mirror_base_url() {
local url="${TARGET_MIRROR_URL%/}"
if [[ "$url" == */current* ]]; then printf '%s' "${url%%/current*}"; else printf '%s' "$url"; fi
}
configure_target_mirror() {
[[ -n "$TARGET_MIRROR_URL" ]] || return 0
local base; base="$(mirror_base_url)"
log "Configuring target XBPS mirror override: $base"
chroot_exec "mkdir -p /etc/xbps.d && cp /usr/share/xbps.d/*-repository-*.conf /etc/xbps.d/ && sed -i 's|https://repo-default.voidlinux.org|$base|g' /etc/xbps.d/*-repository-*.conf && xbps-install -S && xbps-query -L"
}
detect_gpu() {
if [[ "$GPU" != auto ]]; then return 0; fi
if (( DRY_RUN )) || ! command -v lspci >/dev/null 2>&1; then GPU=none; return; fi
local pci; pci="$(lspci -nn | tr '[:upper:]' '[:lower:]')"
case "$pci" in *amd*|*ati*) GPU=amd ;; *nvidia*) GPU=nvidia-nouveau ;; *intel*) GPU=intel ;; *) GPU=none ;; esac
}
normalize_desktop() {
if [[ "$DESKTOP" != none ]]; then
if [[ "$DISPLAY_PROTOCOL" == auto ]]; then
case "$DESKTOP" in sway-wayland) DISPLAY_PROTOCOL=wayland ;; xfce-x11|i3-x11) DISPLAY_PROTOCOL=x11 ;; *) DISPLAY_PROTOCOL=wayland ;; esac
fi
if [[ "$SESSION_MANAGER" == auto ]]; then
if [[ "$DESKTOP" == sway-wayland ]]; then SESSION_MANAGER=seatd; else SESSION_MANAGER=elogind; fi
fi
fi
}
select_options() {
if (( RESUMING )); then normalize_target; normalize_desktop; return; fi
if (( CONFIG_LOADED )); then
[[ -n "$TARGET_DISK" && -n "$HOSTNAME_VALUE" && -n "$TARGET_USER" ]] || die "Config requires TARGET_DISK, HOSTNAME_VALUE and TARGET_USER"
normalize_target; normalize_desktop
if (( DRY_RUN )); then ROOT_PASSWORD='<generated-at-install-time>'; USER_PASSWORD='<generated-at-install-time>'; else ROOT_PASSWORD="$(secure_password root)"; USER_PASSWORD="$(secure_password user)"; fi
return
fi
log "VoidLinux-Guide P0 interactive configuration"
TARGET_ARCH="$(choose 'Target architecture' 'x86_64' 'aarch64')"
TARGET_LIBC="$(choose 'Target libc' 'glibc' 'musl')"
normalize_target
HOSTNAME_VALUE="$(ask 'Hostname' "$HOSTNAME_VALUE")"
TARGET_USER="$(ask 'Ordinary username' "$TARGET_USER")"
TIMEZONE="$(ask 'Timezone (for example Europe/Moscow)' "$TIMEZONE")"
if [[ "$TARGET_LIBC" == glibc ]]; then LOCALE="$(ask 'glibc locale' "$LOCALE")"; else LOCALE='C.UTF-8'; fi
if (( DRY_RUN )); then TARGET_DISK='/dev/sdX'; else
printf '\nAvailable whole disks:\n'; lsblk -d -o NAME,SIZE,MODEL,SERIAL,TRAN,ROTA,TYPE
TARGET_DISK="$(ask 'Whole target disk (for example /dev/sda)')"
fi
if [[ "$BOOT_MODE" == auto ]]; then BOOT_MODE="$(choose 'Boot mode' 'uefi' 'bios')"; fi
[[ "$TARGET_ARCH" != aarch64 || "$BOOT_MODE" == uefi ]] || die 'Generic aarch64 route requires UEFI'
PARTITION_LAYOUT="$(choose 'Partition layout' 'single-root' 'root-home' 'root-home-swap' 'boot-encrypted-home')"
if [[ "$PARTITION_LAYOUT" != single-root ]]; then ROOT_SIZE_GIB="$(ask 'Root size in GiB' "$ROOT_SIZE_GIB")"; fi
if [[ "$PARTITION_LAYOUT" == root-home-swap || "$PARTITION_LAYOUT" == boot-encrypted-home ]]; then SWAP_SIZE_GIB="$(ask 'Swap size in GiB (use 0 for none)' "$SWAP_SIZE_GIB")"; fi
if [[ "$PARTITION_LAYOUT" == boot-encrypted-home ]]; then
BOOT_SIZE_GIB="$(ask 'Plain /boot size in GiB' "$BOOT_SIZE_GIB")"
BOOT_MODE=uefi; ENCRYPTION=luks2-separate
CRYPT_KEY_SIZE="$(choose 'LUKS2 AES-XTS key size' '256' '512')"; CRYPT_CIPHER='aes-xts-plain64'
if (( DRY_RUN )); then
CRYPT_PASSPHRASE='<entered-root-passphrase>'; HOME_CRYPT_PASSPHRASE='<entered-home-passphrase>'
else
local confirm_root confirm_home
read -r -s -p 'LUKS2 root passphrase: ' CRYPT_PASSPHRASE; printf '\n'
read -r -s -p 'Repeat LUKS2 root passphrase: ' confirm_root; printf '\n'
[[ -n "$CRYPT_PASSPHRASE" && "$CRYPT_PASSPHRASE" == "$confirm_root" ]] || die 'LUKS2 root passphrases do not match'
read -r -s -p 'LUKS2 home passphrase: ' HOME_CRYPT_PASSPHRASE; printf '\n'
read -r -s -p 'Repeat LUKS2 home passphrase: ' confirm_home; printf '\n'
[[ -n "$HOME_CRYPT_PASSPHRASE" && "$HOME_CRYPT_PASSPHRASE" == "$confirm_home" ]] || die 'LUKS2 home passphrases do not match'
unset confirm_root confirm_home
fi
else
ENCRYPTION="$(choose 'Encrypt root/home/swap with LUKS1 and LVM?' 'none' 'luks1-lvm')"
if [[ "$ENCRYPTION" == luks1-lvm ]]; then
CRYPT_KEY_SIZE="$(choose 'LUKS AES-XTS key size' '256' '512')"; CRYPT_CIPHER='aes-xts-plain64'
if (( DRY_RUN )); then CRYPT_PASSPHRASE='<entered-at-install-time>'; else
local confirm_pass
read -r -s -p 'LUKS passphrase: ' CRYPT_PASSPHRASE; printf '\n'
read -r -s -p 'Repeat LUKS passphrase: ' confirm_pass; printf '\n'
[[ -n "$CRYPT_PASSPHRASE" && "$CRYPT_PASSPHRASE" == "$confirm_pass" ]] || die 'LUKS passphrases do not match'
unset confirm_pass
fi
fi
fi
DESKTOP="$(choose 'Desktop or window-manager profile' 'none' 'gnome' 'kde-plasma' 'xfce-x11' 'sway-wayland' 'i3-x11')"
if [[ "$DESKTOP" != none ]]; then
case "$DESKTOP" in gnome|kde-plasma|sway-wayland) DISPLAY_PROTOCOL="$(choose 'Display protocol' 'wayland' 'x11')" ;; *) DISPLAY_PROTOCOL=x11 ;; esac
case "$DESKTOP" in sway-wayland) SESSION_MANAGER=seatd ;; *) SESSION_MANAGER=elogind ;; esac
fi
GPU="$(choose 'GPU driver path' 'auto' 'none' 'amd' 'intel' 'nvidia-nouveau' 'nvidia-proprietary')"
if [[ "$GPU" == nvidia-proprietary ]]; then GPU="$(choose 'NVIDIA package family' 'nvidia' 'nvidia580' 'nvidia470' 'nvidia390')"; fi
NETWORK_MANAGER="$(choose 'Install and enable NetworkManager?' 'no' 'yes')"; [[ "$NETWORK_MANAGER" == yes ]] && NETWORK_MANAGER=1 || NETWORK_MANAGER=0
FIREWALL="$(choose 'Firewall policy' 'none' 'nftables')"
APPARMOR="$(choose 'Install and enable AppArmor kernel policy?' 'no' 'yes')"; [[ "$APPARMOR" == yes ]] && APPARMOR=1 || APPARMOR=0
ENABLE_SSH="$(choose 'Install and enable OpenSSH server?' 'no' 'yes')"; [[ "$ENABLE_SSH" == yes ]] && ENABLE_SSH=1 || ENABLE_SSH=0
GENERATE_ROOT_PROFILE="$(choose 'Generate a hardened root profile?' 'yes' 'no')"; [[ "$GENERATE_ROOT_PROFILE" == yes ]] && GENERATE_ROOT_PROFILE=1 || GENERATE_ROOT_PROFILE=0
GENERATE_USER_PROFILE="$(choose 'Generate a non-root profile?' 'yes' 'no')"; [[ "$GENERATE_USER_PROFILE" == yes ]] && GENERATE_USER_PROFILE=1 || GENERATE_USER_PROFILE=0
SHOW_QR="$(choose 'Display generated passwords as terminal QR when qrencode exists?' 'no' 'yes')"; [[ "$SHOW_QR" == yes ]] && SHOW_QR=1 || SHOW_QR=0
if [[ "$PARTITION_LAYOUT" == boot-encrypted-home ]]; then
TPM2_MODE="$(choose 'TPM2 mode (passphrases remain required recovery access)' 'off' 'check' 'clevis-tpm2')"
FIDO2_MODE="$(choose 'FIDO2 mode (readiness only; no automatic enrollment)' 'off' 'check')"
SECURE_BOOT_MODE="$(choose 'Secure Boot mode (prepare does not enroll firmware keys)' 'off' 'check' 'prepare')"
fi
if (( DRY_RUN )); then ROOT_PASSWORD='<generated-at-install-time>'; USER_PASSWORD='<generated-at-install-time>'; else ROOT_PASSWORD="$(secure_password root)"; USER_PASSWORD="$(secure_password user)"; fi
}
validate_inputs() {
[[ "$TARGET_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "Invalid ordinary username"
[[ "$HOSTNAME_VALUE" =~ ^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$ ]] || die "Invalid hostname"
[[ "$TIMEZONE" =~ ^[A-Za-z0-9._+/-]+$ ]] || die "Invalid timezone"
[[ "$LOCALE" =~ ^[A-Za-z0-9_.@-]+$ ]] || die "Invalid locale"
[[ "$TARGET_ROOT" =~ ^/mnt/[A-Za-z0-9._/-]+$ ]] || die "Target root must be a simple /mnt path"
[[ "$REPO_URL" =~ ^https://[a-z0-9.-]*voidlinux\.org/ ]] || die "Repository must be an official Void HTTPS repository"
[[ -z "$TARGET_MIRROR_URL" || "$TARGET_MIRROR_URL" =~ ^https://[a-z0-9.-]*voidlinux\.org(/.*)?$ ]] || die "Mirror must be an official Void HTTPS URL"
[[ "$ENCRYPTION" == none || "$ENCRYPTION" == luks1-lvm || "$ENCRYPTION" == luks2-separate ]] || die "Invalid encryption mode"
[[ "$PARTITION_LAYOUT" =~ ^(single-root|root-home|root-home-swap|boot-encrypted-home)$ ]] || die "Invalid partition layout"
[[ "$DESKTOP" =~ ^(none|gnome|kde-plasma|xfce-x11|sway-wayland|i3-x11)$ ]] || die "Invalid desktop choice"
[[ "$DISPLAY_PROTOCOL" == auto || "$DISPLAY_PROTOCOL" == x11 || "$DISPLAY_PROTOCOL" == wayland ]] || die "Invalid display protocol"
[[ "$SESSION_MANAGER" == auto || "$SESSION_MANAGER" == elogind || "$SESSION_MANAGER" == seatd ]] || die "Invalid session manager"
[[ "$GPU" =~ ^(auto|none|amd|intel|nvidia-nouveau|nvidia|nvidia580|nvidia470|nvidia390)$ ]] || die "Invalid GPU choice"
[[ "$FIREWALL" == none || "$FIREWALL" == nftables ]] || die "Invalid firewall choice"
[[ "$NETWORK_MANAGER" =~ ^[01]$ && "$APPARMOR" =~ ^[01]$ && "$ENABLE_SSH" =~ ^[01]$ ]] || die "Security flags must be 0 or 1"
is_positive_integer "$ESP_SIZE_MIB" || die "ESP_SIZE_MIB must be a positive integer"
is_positive_integer "$BOOT_SIZE_GIB" || die "BOOT_SIZE_GIB must be a positive integer"
if [[ "$PARTITION_LAYOUT" != single-root ]]; then is_positive_integer "$ROOT_SIZE_GIB" || die "ROOT_SIZE_GIB must be a positive integer"; fi
if [[ "$PARTITION_LAYOUT" == root-home-swap ]]; then is_positive_integer "$SWAP_SIZE_GIB" || die "SWAP_SIZE_GIB must be a positive integer"; fi
[[ "$TARGET_LIBC" != musl || ! "$GPU" =~ ^nvidia(580|470|390)?$ ]] || die "Proprietary NVIDIA is unsupported on musl targets"
[[ "$TARGET_ARCH" != aarch64 || "$BOOT_MODE" == uefi ]] || die "Generic aarch64 route requires UEFI"
[[ "$PARTITION_LAYOUT" != boot-encrypted-home || "$BOOT_MODE" == uefi ]] || die "boot-encrypted-home requires UEFI"
[[ "$PARTITION_LAYOUT" != boot-encrypted-home || "$ENCRYPTION" == luks2-separate ]] || die "boot-encrypted-home requires luks2-separate"
[[ "$ENCRYPTION" != luks2-separate || "$PARTITION_LAYOUT" == boot-encrypted-home ]] || die "luks2-separate is only supported by boot-encrypted-home"
[[ "$TPM2_MODE" =~ ^(off|check|clevis-tpm2)$ ]] || die "TPM2_MODE must be off, check or clevis-tpm2"
[[ "$FIDO2_MODE" =~ ^(off|check)$ ]] || die "FIDO2_MODE must be off or check"
[[ "$SECURE_BOOT_MODE" =~ ^(off|check|prepare)$ ]] || die "SECURE_BOOT_MODE must be off, check or prepare"
[[ "$SECURE_BOOT_MODE" == off || "$BOOT_MODE" == uefi ]] || die "Secure Boot requires UEFI"
[[ "$SECURE_BOOT_MODE" == off || "$PARTITION_LAYOUT" == boot-encrypted-home ]] || die "Secure Boot prepare requires the separate /boot profile"
[[ "$TPM2_MODE" == off || "$ENCRYPTION" == luks2-separate ]] || die "TPM2 mode requires the LUKS2 separate /boot profile"
[[ "$FIDO2_MODE" == off || "$ENCRYPTION" == luks2-separate ]] || die "FIDO2 check requires the LUKS2 separate /boot profile"
if [[ "$ENCRYPTION" != none && $CONFIG_LOADED -eq 1 && $DRY_RUN -eq 0 && -z "$CRYPT_PASSPHRASE" ]]; then die "Real encrypted config mode requires CRYPT_PASSPHRASE from a protected input method"; fi
if [[ "$ENCRYPTION" == luks2-separate && $CONFIG_LOADED -eq 1 && $DRY_RUN -eq 0 && -z "$HOME_CRYPT_PASSPHRASE" ]]; then die "Real LUKS2 separate-home config requires HOME_CRYPT_PASSPHRASE through a protected input method"; fi
}
layout_required_gib() {
local required=3
if [[ "$BOOT_MODE" == uefi ]]; then required=$((required + 1)); fi
if [[ "$PARTITION_LAYOUT" == boot-encrypted-home ]]; then
required=$((required + BOOT_SIZE_GIB + ROOT_SIZE_GIB + SWAP_SIZE_GIB + 2))
elif [[ "$PARTITION_LAYOUT" != single-root ]]; then
required=$((required + ROOT_SIZE_GIB + 2))
if [[ "$PARTITION_LAYOUT" == root-home-swap ]]; then required=$((required + SWAP_SIZE_GIB)); fi
fi
printf '%s' "$required"
}
hardware_security_preflight() {
if [[ "$TPM2_MODE" != off ]]; then
log "TPM2 readiness check requested: $TPM2_MODE"
if (( DRY_RUN )); then
printf '%b\n' "${YELLOW}[dry-run]${RESET} inspect /dev/tpmrm0 or /dev/tpm0 and run tpm2_getcap properties-fixed when available"
elif [[ -e /dev/tpmrm0 || -e /dev/tpm0 ]]; then
if command -v tpm2_getcap >/dev/null 2>&1; then tpm2_getcap properties-fixed >/dev/null || die "TPM2 device exists but capability query failed"; fi
log "TPM2 device detected. Existing LUKS passphrases remain mandatory recovery access."
else
die "TPM2 mode was requested but no /dev/tpmrm0 or /dev/tpm0 device is available"
fi
fi
if [[ "$FIDO2_MODE" == check ]]; then
log "FIDO2 readiness check requested"
if (( DRY_RUN )); then
printf '%b\n' "${YELLOW}[dry-run]${RESET} inspect /dev/hidraw* and record libfido2 compatibility boundary"
elif compgen -G '/dev/hidraw*' >/dev/null; then
log "One or more HID raw devices are present; this is a readiness signal, not proof of hmac-secret support."
else
warn "No HID raw devices are present. FIDO2 readiness cannot be established from this live environment."
fi
fi
if [[ "$SECURE_BOOT_MODE" != off ]]; then
log "Secure Boot readiness check requested: $SECURE_BOOT_MODE"
if (( DRY_RUN )); then
printf '%b\n' "${YELLOW}[dry-run]${RESET} inspect /sys/firmware/efi/efivars and stage sbctl status/checklist"
elif [[ -d /sys/firmware/efi/efivars ]]; then
log "UEFI variable filesystem is available. Firmware key enrollment remains an explicit manual post-install action."
else
die "Secure Boot mode requires an EFI runtime environment with /sys/firmware/efi/efivars"
fi
fi
}
print_disk_report() {
log "Read-only target disk report for $TARGET_DISK"
if (( DRY_RUN )) && [[ ! -b "$TARGET_DISK" ]]; then
printf '%s\n' "[dry-run] Placeholder target $TARGET_DISK; no real block-device report is available."
return
fi
printf '%s\n' '--- disk identity ---'
lsblk -d -o PATH,SIZE,MODEL,SERIAL,TRAN,ROTA,TYPE "$TARGET_DISK" || true
printf '%s\n' '--- partition and filesystem view ---'
lsblk -o PATH,SIZE,TYPE,FSTYPE,FSVER,LABEL,UUID,PARTLABEL,PARTTYPE,MOUNTPOINTS "$TARGET_DISK" || true
printf '%s\n' '--- signatures (read-only) ---'
wipefs -n "$TARGET_DISK" || true
printf '%s\n' '--- mounted paths on target disk ---'
findmnt -rn -S "$TARGET_DISK" || true
findmnt -rn -S "${TARGET_DISK}"* || true
printf '%s\n' '--- possible LVM/RAID metadata ---'
if command -v pvs >/dev/null 2>&1; then pvs -o pv_name,vg_name,pv_size --noheadings 2>/dev/null || true; fi
if command -v mdadm >/dev/null 2>&1; then mdadm --examine "$TARGET_DISK" 2>/dev/null || true; fi
}
host_root_disk() {
local source parent
source="$(findmnt -nr -o SOURCE / 2>/dev/null || true)"
[[ "$source" == /dev/* ]] || return 0
parent="$(lsblk -no PKNAME "$source" 2>/dev/null | head -n1 || true)"
if [[ -n "$parent" ]]; then printf '/dev/%s' "$parent"; elif [[ "$(lsblk -no TYPE "$source" 2>/dev/null || true)" == disk ]]; then printf '%s' "$source"; fi
}
target_has_signatures() {
(( DRY_RUN )) && return 1
wipefs -n "$TARGET_DISK" 2>/dev/null | grep -qE 'TYPE|offset'
}
validate_target() {
[[ "$TARGET_DISK" == /dev/* ]] || die "Target disk must be a /dev path"
if (( DRY_RUN )); then return; fi
[[ -b "$TARGET_DISK" ]] || die "Target is not a block device: $TARGET_DISK"
[[ "$(lsblk -dn -o TYPE "$TARGET_DISK")" == disk ]] || die "Target must be a whole disk, not a partition"
[[ "$TARGET_DISK" != /dev/loop* && "$TARGET_DISK" != /dev/ram* ]] || die "Refusing loop or ram device"
local live_disk; live_disk="$(host_root_disk || true)"
[[ -z "$live_disk" || "$live_disk" != "$TARGET_DISK" ]] || die "Refusing disk that hosts the current live root: $TARGET_DISK"
if lsblk -nr -o MOUNTPOINTS "$TARGET_DISK" | grep -q '[^[:space:]]'; then die "Target disk or one of its partitions is mounted"; fi
local size_bytes minimum_bytes
size_bytes="$(lsblk -bdn -o SIZE "$TARGET_DISK")"
minimum_bytes="$(( $(layout_required_gib) * 1024 * 1024 * 1024 ))"
(( size_bytes >= minimum_bytes )) || die "Target disk is too small for the selected layout; need at least $(layout_required_gib) GiB"
}
confirm_existing_signatures() {
if (( DRY_RUN )); then return 0; fi
target_has_signatures || return
warn "Existing filesystem, partition, RAID or LVM signatures were detected on $TARGET_DISK. They will be erased."
if (( NON_INTERACTIVE )); then [[ "$CONFIRM_TOKEN" == VOID_INSTALL ]] || die "Existing signatures require CONFIRM_TOKEN=VOID_INSTALL"; return; fi
local answer
read -r -p "Type ERASE-SIGNATURES to acknowledge existing signatures: " answer
[[ "$answer" == ERASE-SIGNATURES ]] || die "Existing signatures were not acknowledged"
}
package_list() {
detect_gpu
local packages=(base-system sudo xtools)
[[ "$TARGET_ARCH" == aarch64 ]] && packages+=(linux)
[[ "$DESKTOP" == none ]] || packages+=(dbus pipewire)
[[ "$ENCRYPTION" == none ]] || packages+=(cryptsetup lvm2 dracut)
[[ "$SHOW_QR" == 1 ]] && packages+=(qrencode)
[[ "$NETWORK_MANAGER" == 1 ]] && packages+=(NetworkManager)
[[ "$FIREWALL" == nftables ]] && packages+=(nftables runit-nftables)
[[ "$APPARMOR" == 1 ]] && packages+=(apparmor)
[[ "$ENABLE_SSH" == 1 ]] && packages+=(openssh)
[[ "$TPM2_MODE" != off ]] && packages+=(tpm2-tools tpm2-tss)
[[ "$TPM2_MODE" == clevis-tpm2 ]] && packages+=(clevis)
[[ "$FIDO2_MODE" == check ]] && packages+=(libfido2)
[[ "$SECURE_BOOT_MODE" == prepare ]] && packages+=(sbctl)
case "$SESSION_MANAGER" in elogind) packages+=(elogind) ;; seatd) packages+=(seatd) ;; esac
case "$GPU" in amd) packages+=(linux-firmware-amd mesa-dri) ;; intel|nvidia-nouveau) packages+=(mesa-dri) ;; nvidia|nvidia580|nvidia470|nvidia390) packages+=("$GPU") ;; esac
case "$DESKTOP" in gnome) packages+=(gnome gdm) ;; kde-plasma) packages+=(kde-plasma kde-baseapps sddm) ;; xfce-x11) packages+=(xorg xfce4 lightdm) ;; sway-wayland) packages+=(sway xorg-server-xwayland) ;; i3-x11) packages+=(xorg i3 lightdm) ;; esac
[[ "$DISPLAY_PROTOCOL" == x11 && "$DESKTOP" == gnome ]] && packages+=(xorg)
[[ "$DISPLAY_PROTOCOL" == x11 && "$DESKTOP" == kde-plasma ]] && packages+=(xorg)
printf '%s ' "${packages[@]}"
}
summary() {
detect_gpu
state_file_default
cat <<SUMMARY
----- VoidLinux-Guide P1 installation plan -----
Installer: $INSTALLER_VERSION
Target: $TARGET_ARCH / $TARGET_LIBC ($XBPS_TARGET)
Repository: $REPO_URL
Mirror override: ${TARGET_MIRROR_URL:-none}
Target disk: $TARGET_DISK
Boot mode: $BOOT_MODE
Layout: $PARTITION_LAYOUT (root ${ROOT_SIZE_GIB}GiB, boot ${BOOT_SIZE_GIB}GiB, swap ${SWAP_SIZE_GIB}GiB when applicable)
Target root: $TARGET_ROOT
Encryption: $ENCRYPTION${ENCRYPTION:+ ($CRYPT_CIPHER / key-size $CRYPT_KEY_SIZE)}
TPM2 / FIDO2: $TPM2_MODE / $FIDO2_MODE
Secure Boot: $SECURE_BOOT_MODE
State file: $STATE_FILE
Hostname: $HOSTNAME_VALUE
User: $TARGET_USER
Locale/timezone: $LOCALE / $TIMEZONE
Desktop: $DESKTOP
Display/session: $DISPLAY_PROTOCOL / $SESSION_MANAGER
GPU: $GPU
NetworkManager: $NETWORK_MANAGER
Firewall/AppArmor: $FIREWALL / $APPARMOR
OpenSSH: $ENABLE_SSH
Packages: $(package_list)
Backup directory: ${BACKUP_DIR:-/root/voidlinux-guide-install-backup-<timestamp>}
--------------------------------------------------
SUMMARY
}
partition_spec() {
if [[ "$PARTITION_LAYOUT" == boot-encrypted-home ]]; then
printf 'label: gpt\n'
printf ',%sM,U\n' "$ESP_SIZE_MIB"
printf ',%sG,L\n' "$BOOT_SIZE_GIB"
printf ',%sG,L\n' "$ROOT_SIZE_GIB"
printf ',,L\n'
if (( SWAP_SIZE_GIB > 0 )); then printf ',%sG,S\n' "$SWAP_SIZE_GIB"; fi
return
fi
if [[ "$BOOT_MODE" == uefi ]]; then
printf 'label: gpt\n'
printf ',%sM,U\n' "$ESP_SIZE_MIB"
if [[ "$ENCRYPTION" != none || "$PARTITION_LAYOUT" == single-root ]]; then printf ',,L\n'; return; fi
printf ',%sG,L\n' "$ROOT_SIZE_GIB"
if [[ "$PARTITION_LAYOUT" == root-home-swap ]]; then printf ',%sG,S\n' "$SWAP_SIZE_GIB"; fi
printf ',,L\n'
else
printf 'label: dos\n'
if [[ "$ENCRYPTION" != none || "$PARTITION_LAYOUT" == single-root ]]; then printf ',,83\n'; return; fi
printf ',%sG,83\n' "$ROOT_SIZE_GIB"
if [[ "$PARTITION_LAYOUT" == root-home-swap ]]; then printf ',%sG,82\n' "$SWAP_SIZE_GIB"; fi
printf ',,83\n'
fi
}
# Assign physical devices after creating the selected layout.
assign_layout_devices() {
local base=1
ESP_DEV=''; BOOT_DEV=''; DATA_DEV=''; ROOT_DATA_DEV=''; HOME_DATA_DEV=''; ROOT_DEV=''; HOME_DEV=''; SWAP_DEV=''
if [[ "$PARTITION_LAYOUT" == boot-encrypted-home ]]; then
ESP_DEV="$(part_path "$TARGET_DISK" 1)"
BOOT_DEV="$(part_path "$TARGET_DISK" 2)"
ROOT_DATA_DEV="$(part_path "$TARGET_DISK" 3)"
HOME_DATA_DEV="$(part_path "$TARGET_DISK" 4)"
if (( SWAP_SIZE_GIB > 0 )); then SWAP_DEV="$(part_path "$TARGET_DISK" 5)"; fi
return
fi
if [[ "$BOOT_MODE" == uefi ]]; then ESP_DEV="$(part_path "$TARGET_DISK" 1)"; base=2; fi
if [[ "$ENCRYPTION" != none ]]; then DATA_DEV="$(part_path "$TARGET_DISK" "$base")"; return; fi
ROOT_DEV="$(part_path "$TARGET_DISK" "$base")"
case "$PARTITION_LAYOUT" in
single-root) ;;
root-home) HOME_DEV="$(part_path "$TARGET_DISK" "$((base + 1))")" ;;
root-home-swap) SWAP_DEV="$(part_path "$TARGET_DISK" "$((base + 1))")"; HOME_DEV="$(part_path "$TARGET_DISK" "$((base + 2))")" ;;
esac
}
format_and_mount_plain() {
run mkfs.ext4 -F "$ROOT_DEV"
[[ -z "$HOME_DEV" ]] || run mkfs.ext4 -F "$HOME_DEV"
[[ -z "$SWAP_DEV" ]] || { run mkswap "$SWAP_DEV"; run swapon "$SWAP_DEV"; }
run mkdir -p "$TARGET_ROOT"
run mount "$ROOT_DEV" "$TARGET_ROOT"
if [[ -n "$HOME_DEV" ]]; then run mkdir -p "$TARGET_ROOT/home"; run mount "$HOME_DEV" "$TARGET_ROOT/home"; fi
if [[ -n "$ESP_DEV" ]]; then run mkfs.vfat -F32 "$ESP_DEV"; run mkdir -p "$TARGET_ROOT/boot/efi"; run mount "$ESP_DEV" "$TARGET_ROOT/boot/efi"; fi
}
format_and_mount_encrypted() {
if (( DRY_RUN )); then
run cryptsetup luksFormat --type luks1 --cipher "$CRYPT_CIPHER" --key-size "$CRYPT_KEY_SIZE" "$DATA_DEV"
run cryptsetup luksOpen "$DATA_DEV" "$VG_NAME"
run pvcreate "/dev/mapper/$VG_NAME"; run vgcreate "$VG_NAME" "/dev/mapper/$VG_NAME"
if [[ "$PARTITION_LAYOUT" == single-root ]]; then
run lvcreate --name "$ROOT_LV_NAME" -l 100%FREE "$VG_NAME"
else
run lvcreate --name "$ROOT_LV_NAME" -L "${ROOT_SIZE_GIB}G" "$VG_NAME"
if [[ "$PARTITION_LAYOUT" == root-home-swap ]]; then run lvcreate --name "$SWAP_LV_NAME" -L "${SWAP_SIZE_GIB}G" "$VG_NAME"; fi
run lvcreate --name "$HOME_LV_NAME" -l 100%FREE "$VG_NAME"
fi
else
printf '%s\n' "$CRYPT_PASSPHRASE" | cryptsetup luksFormat --batch-mode --type luks1 --cipher "$CRYPT_CIPHER" --key-size "$CRYPT_KEY_SIZE" "$DATA_DEV" -
printf '%s\n' "$CRYPT_PASSPHRASE" | cryptsetup luksOpen "$DATA_DEV" "$VG_NAME" --key-file=-
pvcreate "/dev/mapper/$VG_NAME"; vgcreate "$VG_NAME" "/dev/mapper/$VG_NAME"
if [[ "$PARTITION_LAYOUT" == single-root ]]; then lvcreate --name "$ROOT_LV_NAME" -l 100%FREE "$VG_NAME"
else
lvcreate --name "$ROOT_LV_NAME" -L "${ROOT_SIZE_GIB}G" "$VG_NAME"
[[ "$PARTITION_LAYOUT" != root-home-swap ]] || lvcreate --name "$SWAP_LV_NAME" -L "${SWAP_SIZE_GIB}G" "$VG_NAME"
lvcreate --name "$HOME_LV_NAME" -l 100%FREE "$VG_NAME"
fi
fi
ROOT_DEV="/dev/$VG_NAME/$ROOT_LV_NAME"
[[ "$PARTITION_LAYOUT" == single-root ]] || HOME_DEV="/dev/$VG_NAME/$HOME_LV_NAME"
[[ "$PARTITION_LAYOUT" == root-home-swap ]] && SWAP_DEV="/dev/$VG_NAME/$SWAP_LV_NAME"
run mkfs.ext4 -F "$ROOT_DEV"
[[ -z "$HOME_DEV" ]] || run mkfs.ext4 -F "$HOME_DEV"
[[ -z "$SWAP_DEV" ]] || { run mkswap "$SWAP_DEV"; run swapon "$SWAP_DEV"; }
run mkdir -p "$TARGET_ROOT"; run mount "$ROOT_DEV" "$TARGET_ROOT"
if [[ -n "$HOME_DEV" ]]; then run mkdir -p "$TARGET_ROOT/home"; run mount "$HOME_DEV" "$TARGET_ROOT/home"; fi
if [[ -n "$ESP_DEV" ]]; then run mkfs.vfat -F32 "$ESP_DEV"; run mkdir -p "$TARGET_ROOT/boot/efi"; run mount "$ESP_DEV" "$TARGET_ROOT/boot/efi"; fi
if (( ! DRY_RUN )); then
CRYPT_UUID="$(blkid -s UUID -o value "$DATA_DEV")"
mkdir -p "$TARGET_ROOT/etc"
printf '%s UUID=%s none luks\n' "$VG_NAME" "$CRYPT_UUID" > "$TARGET_ROOT/etc/crypttab"
fi
}
format_and_mount_luks2_separate() {
if (( DRY_RUN )); then
run cryptsetup luksFormat --type luks2 --cipher "$CRYPT_CIPHER" --key-size "$CRYPT_KEY_SIZE" "$ROOT_DATA_DEV"
run cryptsetup open "$ROOT_DATA_DEV" "$ROOT_CRYPT_NAME"
run cryptsetup luksFormat --type luks2 --cipher "$CRYPT_CIPHER" --key-size "$CRYPT_KEY_SIZE" "$HOME_DATA_DEV"
run cryptsetup open "$HOME_DATA_DEV" "$HOME_CRYPT_NAME"
else
printf '%s\n' "$CRYPT_PASSPHRASE" | cryptsetup luksFormat --batch-mode --type luks2 --cipher "$CRYPT_CIPHER" --key-size "$CRYPT_KEY_SIZE" "$ROOT_DATA_DEV" -
printf '%s\n' "$CRYPT_PASSPHRASE" | cryptsetup open "$ROOT_DATA_DEV" "$ROOT_CRYPT_NAME" --key-file=-
printf '%s\n' "$HOME_CRYPT_PASSPHRASE" | cryptsetup luksFormat --batch-mode --type luks2 --cipher "$CRYPT_CIPHER" --key-size "$CRYPT_KEY_SIZE" "$HOME_DATA_DEV" -
printf '%s\n' "$HOME_CRYPT_PASSPHRASE" | cryptsetup open "$HOME_DATA_DEV" "$HOME_CRYPT_NAME" --key-file=-
fi
ROOT_DEV="/dev/mapper/$ROOT_CRYPT_NAME"
HOME_DEV="/dev/mapper/$HOME_CRYPT_NAME"
run mkfs.ext4 -F "$ROOT_DEV"; run mkfs.ext4 -F "$HOME_DEV"; run mkfs.ext4 -F "$BOOT_DEV"; run mkfs.vfat -F32 "$ESP_DEV"
if [[ -n "$SWAP_DEV" ]]; then run mkswap "$SWAP_DEV"; run swapon "$SWAP_DEV"; fi
run mkdir -p "$TARGET_ROOT"; run mount "$ROOT_DEV" "$TARGET_ROOT"
run mkdir -p "$TARGET_ROOT/boot" "$TARGET_ROOT/boot/efi" "$TARGET_ROOT/home"
run mount "$BOOT_DEV" "$TARGET_ROOT/boot"; run mount "$ESP_DEV" "$TARGET_ROOT/boot/efi"; run mount "$HOME_DEV" "$TARGET_ROOT/home"
if (( DRY_RUN )); then
CRYPT_UUID='<root-luks2-uuid>'; HOME_CRYPT_UUID='<home-luks2-uuid>'
else
CRYPT_UUID="$(blkid -s UUID -o value "$ROOT_DATA_DEV")"
HOME_CRYPT_UUID="$(blkid -s UUID -o value "$HOME_DATA_DEV")"
mkdir -p "$TARGET_ROOT/etc"
{
printf '%s UUID=%s none luks\n' "$ROOT_CRYPT_NAME" "$CRYPT_UUID"
printf '%s UUID=%s none luks\n' "$HOME_CRYPT_NAME" "$HOME_CRYPT_UUID"
} > "$TARGET_ROOT/etc/crypttab"
fi
}
partition_target() {
log "Creating $PARTITION_LAYOUT layout on $TARGET_DISK (destructive)"
assign_layout_devices
if (( DRY_RUN )); then run_shell "partition_spec | sfdisk --wipe always $TARGET_DISK"; else partition_spec | sfdisk --wipe always "$TARGET_DISK"; partprobe "$TARGET_DISK"; fi
if [[ "$ENCRYPTION" == none ]]; then
format_and_mount_plain
elif [[ "$ENCRYPTION" == luks1-lvm ]]; then
format_and_mount_encrypted
else
format_and_mount_luks2_separate
fi
}
resume_open_and_mount() {
step_done partition || return
log "Mounting existing incomplete target for resume"
if [[ "$ENCRYPTION" == luks1-lvm ]]; then
if (( DRY_RUN )); then
CRYPT_PASSPHRASE='<prompted-on-resume>'
elif (( NON_INTERACTIVE )); then
[[ -n "$CRYPT_PASSPHRASE" ]] || die "Non-interactive encrypted resume requires CRYPT_PASSPHRASE through a protected input method"
else
read -r -s -p 'LUKS passphrase for resume: ' CRYPT_PASSPHRASE
printf '\n'
fi
run cryptsetup luksOpen "$DATA_DEV" "$VG_NAME"; run vgchange -ay "$VG_NAME"
elif [[ "$ENCRYPTION" == luks2-separate ]]; then
if (( DRY_RUN )); then
CRYPT_PASSPHRASE='<prompted-root-passphrase>'; HOME_CRYPT_PASSPHRASE='<prompted-home-passphrase>'
elif (( NON_INTERACTIVE )); then
[[ -n "$CRYPT_PASSPHRASE" && -n "$HOME_CRYPT_PASSPHRASE" ]] || die "Non-interactive LUKS2 resume requires root and home passphrases through protected input"
else
read -r -s -p 'LUKS2 root passphrase for resume: ' CRYPT_PASSPHRASE; printf '\n'
read -r -s -p 'LUKS2 home passphrase for resume: ' HOME_CRYPT_PASSPHRASE; printf '\n'
fi
run cryptsetup open "$ROOT_DATA_DEV" "$ROOT_CRYPT_NAME"; run cryptsetup open "$HOME_DATA_DEV" "$HOME_CRYPT_NAME"
fi
run mkdir -p "$TARGET_ROOT"
run mount "$ROOT_DEV" "$TARGET_ROOT"
if [[ -n "$BOOT_DEV" ]]; then run mkdir -p "$TARGET_ROOT/boot"; run mount "$BOOT_DEV" "$TARGET_ROOT/boot"; fi
if [[ -n "$HOME_DEV" ]]; then run mkdir -p "$TARGET_ROOT/home"; run mount "$HOME_DEV" "$TARGET_ROOT/home"; fi
if [[ -n "$ESP_DEV" ]]; then run mkdir -p "$TARGET_ROOT/boot/efi"; run mount "$ESP_DEV" "$TARGET_ROOT/boot/efi"; fi
[[ -z "$SWAP_DEV" ]] || run swapon "$SWAP_DEV"
}
mount_chroot_api() {
run mkdir -p "$TARGET_ROOT/dev" "$TARGET_ROOT/proc" "$TARGET_ROOT/sys" "$TARGET_ROOT/run"
if (( DRY_RUN )); then
run mount --rbind /dev "$TARGET_ROOT/dev"; run mount --make-rslave "$TARGET_ROOT/dev"
run mount -t proc proc "$TARGET_ROOT/proc"
run mount --rbind /sys "$TARGET_ROOT/sys"; run mount --make-rslave "$TARGET_ROOT/sys"
run mount --rbind /run "$TARGET_ROOT/run"; run mount --make-rslave "$TARGET_ROOT/run"
else
mount --rbind /dev "$TARGET_ROOT/dev"; mount --make-rslave "$TARGET_ROOT/dev"
mount -t proc proc "$TARGET_ROOT/proc"
mount --rbind /sys "$TARGET_ROOT/sys"; mount --make-rslave "$TARGET_ROOT/sys"
mount --rbind /run "$TARGET_ROOT/run"; mount --make-rslave "$TARGET_ROOT/run"
fi
}
bootstrap_target() {
log "Bootstrapping $XBPS_TARGET through the official XBPS repository"
if (( DRY_RUN )); then
run mkdir -p "$TARGET_ROOT/var/db/xbps/keys"; run cp -a /var/db/xbps/keys/. "$TARGET_ROOT/var/db/xbps/keys/"
else
mkdir -p "$TARGET_ROOT/var/db/xbps/keys"; cp -a /var/db/xbps/keys/. "$TARGET_ROOT/var/db/xbps/keys/"
fi
run env XBPS_ARCH="$XBPS_TARGET" xbps-install -S -r "$TARGET_ROOT" -R "$REPO_URL" base-system
}
write_target_file() {
local path="$1"; shift
if (( DRY_RUN )); then printf '%b\n' "${YELLOW}[dry-run]${RESET} write $TARGET_ROOT/$path"; cat
else mkdir -p "$(dirname "$TARGET_ROOT/$path")"; cat > "$TARGET_ROOT/$path"; fi
}
chroot_exec() {
if (( DRY_RUN )); then printf '%b\n' "${YELLOW}[dry-run]${RESET} chroot $TARGET_ROOT $*"; else chroot "$TARGET_ROOT" /bin/sh -c "$*"; fi
}
enable_service() {
local service="$1"
if (( DRY_RUN )); then run ln -s "/etc/sv/$service" "$TARGET_ROOT/var/service/"
else mkdir -p "$TARGET_ROOT/var/service"; ln -s "/etc/sv/$service" "$TARGET_ROOT/var/service/" 2>/dev/null || true; fi
}
device_uuid() {
if (( DRY_RUN )); then printf '<%s-uuid>' "$1"; else blkid -s UUID -o value "$1"; fi
}
configure_target() {
log "Writing target configuration"
write_target_file /etc/hostname <<EOF
$HOSTNAME_VALUE
EOF
write_target_file /etc/locale.conf <<EOF
LANG=$LOCALE
EOF
if (( DRY_RUN )); then run ln -sf "/usr/share/zoneinfo/$TIMEZONE" "$TARGET_ROOT/etc/localtime"; else ln -sf "/usr/share/zoneinfo/$TIMEZONE" "$TARGET_ROOT/etc/localtime"; fi
local fstab_content root_uuid
root_uuid="$(device_uuid "$ROOT_DEV")"
fstab_content="UUID=$root_uuid / ext4 defaults 0 1"
if [[ -n "$BOOT_DEV" ]]; then fstab_content+=$'\n'"UUID=$(device_uuid "$BOOT_DEV") /boot ext4 defaults 0 2"; fi
if [[ -n "$HOME_DEV" ]]; then fstab_content+=$'\n'"UUID=$(device_uuid "$HOME_DEV") /home ext4 defaults 0 2"; fi
if [[ -n "$SWAP_DEV" ]]; then fstab_content+=$'\n'"UUID=$(device_uuid "$SWAP_DEV") none swap defaults 0 0"; fi
if [[ -n "$ESP_DEV" ]]; then fstab_content+=$'\n'"UUID=$(device_uuid "$ESP_DEV") /boot/efi vfat defaults 0 2"; fi
write_target_file /etc/fstab <<< "$fstab_content"
configure_target_mirror
if [[ "$ENCRYPTION" == luks1-lvm ]]; then
write_target_file /etc/dracut.conf.d/10-voidlinux-guide-crypt.conf <<'EOF'
add_dracutmodules+=" crypt lvm "
EOF
elif [[ "$ENCRYPTION" == luks2-separate ]]; then
write_target_file /etc/dracut.conf.d/10-voidlinux-guide-crypt.conf <<'EOF'
add_dracutmodules+=" crypt "
EOF
write_target_file /etc/default/grub <<EOF
GRUB_CMDLINE_LINUX_DEFAULT="rd.luks.uuid=luks-$CRYPT_UUID root=/dev/mapper/$ROOT_CRYPT_NAME"
EOF
fi
write_target_file /etc/profile.d/voidlinux-guide.sh <<'EOF'
# Safe defaults generated by VoidLinux-Guide.
umask 077
export EDITOR="${EDITOR:-vi}"
EOF
if (( GENERATE_ROOT_PROFILE )); then write_target_file /root/.profile <<'EOF'
# Generated by VoidLinux-Guide.
umask 077
export EDITOR="${EDITOR:-vi}"
alias ll='ls -alF'
EOF
fi
if (( GENERATE_USER_PROFILE )); then
write_target_file "/home/$TARGET_USER/.profile" <<'EOF'
# Generated by VoidLinux-Guide.
umask 077
export EDITOR="${EDITOR:-vi}"
alias ll='ls -alF'
EOF
fi
if (( APPARMOR )); then
if (( DRY_RUN )); then printf '%b\n' "${YELLOW}[dry-run]${RESET} ensure GRUB_CMDLINE_LINUX_DEFAULT contains apparmor=1 security=apparmor"
else
mkdir -p "$TARGET_ROOT/etc/default"; touch "$TARGET_ROOT/etc/default/grub"
if grep -q '^GRUB_CMDLINE_LINUX_DEFAULT=' "$TARGET_ROOT/etc/default/grub"; then sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT="apparmor=1 security=apparmor"/' "$TARGET_ROOT/etc/default/grub"
else printf '%s\n' 'GRUB_CMDLINE_LINUX_DEFAULT="apparmor=1 security=apparmor"' >> "$TARGET_ROOT/etc/default/grub"; fi
fi
fi
if [[ "$FIREWALL" == nftables ]]; then write_target_file /etc/nftables.conf <<'EOF'
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iifname "lo" accept
ct state established,related accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
udp sport 67 udp dport 68 accept
udp sport 547 udp dport 546 accept