-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathruntime.func
More file actions
2583 lines (2267 loc) · 89.3 KB
/
Copy pathruntime.func
File metadata and controls
2583 lines (2267 loc) · 89.3 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
# Copyright (c) 2021-2026 community-scripts ORG
# License: MIT | https://raw.githubusercontent.com/community-scripts/core/main/LICENSE
# ==============================================================================
# TOOLS/RUNTIME.FUNC - LANGUAGE RUNTIMES AND APPLICATION INSTALLERS
# ==============================================================================
# Part of the shared helper library. Load lib/tools.func -- it sources every
# part -- rather than this file directly; the split is for maintenance only and
# no function name or behaviour changed with it.
# ==============================================================================
[[ -n "${_TOOLS_RUNTIME_LOADED:-}" ]] && return 0
_TOOLS_RUNTIME_LOADED=1
setup_adminer() {
if grep -qi alpine /etc/os-release; then
msg_info "Setup Adminer (Alpine)"
mkdir -p /var/www/localhost/htdocs/adminer
if ! curl_with_retry "https://github.com/vrana/adminer/releases/latest/download/adminer.php" "/var/www/localhost/htdocs/adminer/index.php"; then
msg_error "Failed to download Adminer"
msg_error "Hint: Check connectivity to github.com/vrana/adminer (GitHub Releases)"
return 250
fi
cache_installed_version "adminer" "latest-alpine"
msg_ok "Setup Adminer (Alpine)"
else
msg_info "Setup Adminer (Debian/Ubuntu)"
ensure_dependencies adminer
$STD a2enconf adminer || {
msg_error "Failed to enable Adminer Apache config"
return 150
}
$STD systemctl reload apache2 || {
msg_error "Failed to reload Apache"
return 150
}
local VERSION
VERSION=$(dpkg -s adminer 2>/dev/null | grep '^Version:' | awk '{print $2}' 2>/dev/null || echo 'unknown')
cache_installed_version "adminer" "${VERSION:-unknown}"
msg_ok "Setup Adminer (Debian/Ubuntu)"
fi
}
# ------------------------------------------------------------------------------
# Installs or upgrades ClickHouse database server.
#
# Description:
# - Adds ClickHouse official repository
# - Installs specified version
# - Configures systemd service
# - Supports Debian/Ubuntu with fallback mechanism
#
# Variables:
# CLICKHOUSE_VERSION - ClickHouse version to install (default: latest)
# ------------------------------------------------------------------------------
setup_composer() {
local COMPOSER_BIN="/usr/local/bin/composer"
export COMPOSER_ALLOW_SUPERUSER=1
# Get currently installed version
local INSTALLED_VERSION=""
if [[ -x "$COMPOSER_BIN" ]]; then
INSTALLED_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}')
fi
# Scenario 1: Already installed - just self-update
if [[ -n "$INSTALLED_VERSION" ]]; then
msg_info "Update Composer $INSTALLED_VERSION"
$STD "$COMPOSER_BIN" self-update --no-interaction || {
msg_warn "Composer self-update failed, continuing with current version"
}
local UPDATED_VERSION
UPDATED_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}')
cache_installed_version "composer" "$UPDATED_VERSION"
msg_ok "Update Composer $UPDATED_VERSION"
return 0
fi
# Scenario 2: Fresh install
msg_info "Setup Composer"
for old in /usr/bin/composer /bin/composer /root/.composer/vendor/bin/composer; do
[[ -e "$old" && "$old" != "$COMPOSER_BIN" ]] && rm -f "$old"
done
ensure_usr_local_bin_persist
export PATH="/usr/local/bin:$PATH"
if ! curl_with_retry "https://getcomposer.org/installer" "/tmp/composer-setup.php"; then
msg_error "Failed to download Composer installer"
msg_error "Hint: Check connectivity to getcomposer.org"
return 250
fi
$STD php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer || {
msg_error "Failed to install Composer"
rm -f /tmp/composer-setup.php
return 150
}
rm -f /tmp/composer-setup.php
if [[ ! -x "$COMPOSER_BIN" ]]; then
msg_error "Composer installation failed"
return 127
fi
chmod +x "$COMPOSER_BIN"
$STD "$COMPOSER_BIN" self-update --no-interaction || {
msg_warn "Composer self-update failed after fresh install"
}
local FINAL_VERSION
FINAL_VERSION=$("$COMPOSER_BIN" --version 2>/dev/null | awk '{print $3}')
cache_installed_version "composer" "$FINAL_VERSION"
msg_ok "Setup Composer"
}
# ------------------------------------------------------------------------------
# Docker Engine Installation and Management (All-In-One)
#
# Description:
# - By default uses distro repository (docker.io) for stability
# - Optionally uses official Docker repository for latest features
# - Detects and migrates old Docker installations
# - Optional: Installs/Updates Portainer CE
# - Updates running containers interactively
# - Cleans up legacy repository files
#
# Usage:
# setup_docker # Uses official Docker repo (recommended)
# USE_DOCKER_REPO=false setup_docker # Uses distro docker.io package
# DOCKER_PORTAINER="true" setup_docker
# DOCKER_LOG_DRIVER="json-file" setup_docker
#
# Variables:
# USE_DOCKER_REPO - Set to "false" to use distro docker.io package
# (default: true, uses official Docker repository)
# DOCKER_PORTAINER - Install Portainer CE (optional, "true" to enable)
# DOCKER_LOG_DRIVER - Log driver (optional, default: "journald")
# DOCKER_SKIP_UPDATES - Skip container update check (optional, "true" to skip)
#
# Features:
# - Uses official Docker repository by default
# - Migrates from get.docker.com to repository-based installation
# - Updates Docker Engine if newer version available
# - Interactive per-container update prompt (Y/N, 60 s auto-no)
# - Compose-managed containers: full restart via docker compose
# - Standalone containers: image pull only, no destructive stop/rm
# - Portainer installation and update support
# - Set DOCKER_NONINTERACTIVE=1 to skip interactive prompts (CI/unattended)
# ------------------------------------------------------------------------------
_docker_is_noninteractive() {
[[ "${DOCKER_NONINTERACTIVE:-}" == "1" || "${DOCKER_NONINTERACTIVE:-}" == "true" || "${DOCKER_NONINTERACTIVE:-}" == "TRUE" ]] || [[ ! -t 0 ]]
}
# ------------------------------------------------------------------------------
# ensure_docker()
#
# - Docker precondition for addon scripts that ship a compose stack
# - Reuses an existing installation, otherwise offers to install Docker
# (Debian/Ubuntu via setup_docker, Alpine via apk)
# - Aborts when the user declines or Compose is unavailable afterwards
# ------------------------------------------------------------------------------
ensure_docker() {
if command -v docker &>/dev/null; then
msg_ok "Docker $(docker --version | cut -d' ' -f3 | tr -d ',') is available"
if docker compose version &>/dev/null; then
msg_ok "Docker Compose is available"
return 0
fi
msg_error "Docker Compose plugin is not available. Please install it."
exit 237
fi
msg_warn "Docker is not installed."
echo -n "${TAB:- }Install Docker now? (y/N): "
local install_docker_prompt
read -r install_docker_prompt
if [[ ! "${install_docker_prompt,,}" =~ ^(y|yes)$ ]]; then
msg_error "Docker is required for ${APP:-this script}. Exiting."
exit 254
fi
if is_alpine; then
msg_info "Installing Docker"
$STD apk add --no-cache docker docker-cli-compose
$STD rc-update add docker default
$STD rc-service docker start
msg_ok "Installed Docker"
else
setup_docker || {
msg_error "Docker installation failed."
exit 237
}
fi
if ! command -v docker &>/dev/null || ! docker compose version &>/dev/null; then
msg_error "Docker or the Compose plugin is still unavailable after installation."
exit 237
fi
}
setup_docker() {
local docker_installed=false
local portainer_installed=false
local USE_DOCKER_REPO="${USE_DOCKER_REPO:-true}"
# Check if Docker is already installed
if command -v docker &>/dev/null; then
docker_installed=true
DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_info "Docker $DOCKER_CURRENT_VERSION detected"
fi
# Check if Portainer is running
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^portainer$'; then
portainer_installed=true
msg_info "Portainer container detected"
fi
# Scenario 1: Use distro repository (opt-out via USE_DOCKER_REPO=false)
if [[ "$USE_DOCKER_REPO" != "true" && "$USE_DOCKER_REPO" != "TRUE" && "$USE_DOCKER_REPO" != "1" ]]; then
# Install or upgrade Docker from distro repo
if [ "$docker_installed" = true ]; then
msg_info "Checking for Docker updates (distro package)"
ensure_apt_working || return 100
upgrade_packages_with_retry "docker.io" "docker-compose" || true
DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_ok "Docker is up-to-date ($DOCKER_CURRENT_VERSION)"
else
msg_info "Installing Docker (distro package)"
ensure_apt_working || return 100
# Install docker.io and docker-compose from distro
if ! install_packages_with_retry "docker.io"; then
msg_error "Failed to install docker.io from distro repository"
return 100
fi
# docker-compose is optional
$STD apt install -y docker-compose 2>/dev/null ||
msg_warn "Optional docker-compose not available from distro repository — use 'docker compose' plugin instead if needed"
DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_ok "Installed Docker $DOCKER_CURRENT_VERSION (distro package)"
fi
# Configure daemon.json
local log_driver="${DOCKER_LOG_DRIVER:-journald}"
mkdir -p /etc/docker
if [ ! -f /etc/docker/daemon.json ]; then
cat <<EOF >/etc/docker/daemon.json
{
"log-driver": "$log_driver"
}
EOF
fi
# Enable and start Docker
systemctl enable -q --now docker
# Continue to Portainer section below
else
# Scenario 2: Use official Docker repository (USE_DOCKER_REPO=true)
# Cleanup old repository configurations
if [ -f /etc/apt/sources.list.d/docker.list ]; then
msg_info "Migrating from old Docker repository format"
rm -f /etc/apt/sources.list.d/docker.list
rm -f /etc/apt/keyrings/docker.asc
fi
# Setup/Update Docker repository
msg_info "Setting up Docker Repository"
setup_deb822_repo \
"docker" \
"https://download.docker.com/linux/$(get_os_info id)/gpg" \
"https://download.docker.com/linux/$(get_os_info id)" \
"$(get_os_info codename)" \
"stable" \
"$(dpkg --print-architecture)"
# Install or upgrade Docker
if [ "$docker_installed" = true ]; then
msg_info "Checking for Docker updates"
DOCKER_LATEST_VERSION=$(apt-cache policy docker-ce | grep Candidate | awk '{print $2}' 2>/dev/null | cut -d':' -f2 | cut -d'-' -f1 || echo '')
if [ "$DOCKER_CURRENT_VERSION" != "$DOCKER_LATEST_VERSION" ]; then
msg_info "Updating Docker $DOCKER_CURRENT_VERSION → $DOCKER_LATEST_VERSION"
$STD apt install -y --only-upgrade \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin || {
msg_error "Failed to update Docker packages"
return 100
}
msg_ok "Updated Docker to $DOCKER_LATEST_VERSION"
else
msg_ok "Docker is up-to-date ($DOCKER_CURRENT_VERSION)"
fi
else
msg_info "Installing Docker"
$STD apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin || {
msg_error "Failed to install Docker packages"
return 100
}
DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
msg_ok "Installed Docker $DOCKER_CURRENT_VERSION"
fi
# Configure daemon.json
local log_driver="${DOCKER_LOG_DRIVER:-journald}"
mkdir -p /etc/docker
if [ ! -f /etc/docker/daemon.json ]; then
cat <<EOF >/etc/docker/daemon.json
{
"log-driver": "$log_driver"
}
EOF
fi
# Enable and start Docker
systemctl enable -q --now docker
fi
# Portainer Management (common for both modes)
if [[ "${DOCKER_PORTAINER:-}" == "true" ]]; then
if [ "$portainer_installed" = true ]; then
msg_info "Checking for Portainer updates"
PORTAINER_CURRENT=$(docker inspect portainer --format='{{.Config.Image}}' 2>/dev/null | cut -d':' -f2)
PORTAINER_LATEST=$(curl -fsSL https://registry.hub.docker.com/v2/repositories/portainer/portainer-ce/tags?page_size=100 | grep -oP '"name":"\K[0-9]+\.[0-9]+\.[0-9]+"' | head -1 | tr -d '"')
if [ "$PORTAINER_CURRENT" != "$PORTAINER_LATEST" ]; then
if _docker_is_noninteractive; then
msg_info "Skipping Portainer update prompt (non-interactive)"
else
read -r -p "${TAB3}Update Portainer $PORTAINER_CURRENT → $PORTAINER_LATEST? <y/N> " prompt </dev/tty
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
msg_info "Updating Portainer"
docker stop portainer
docker rm portainer
docker pull portainer/portainer-ce:latest
docker run -d \
-p 9000:9000 \
-p 9443:9443 \
--name=portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
msg_ok "Updated Portainer to $PORTAINER_LATEST"
fi
fi
else
msg_ok "Portainer is up-to-date ($PORTAINER_CURRENT)"
fi
else
msg_info "Installing Portainer"
docker volume create portainer_data
docker run -d \
-p 9000:9000 \
-p 9443:9443 \
--name=portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
LOCAL_IP=$(hostname -I | awk '{print $1}')
msg_ok "Installed Portainer (http://${LOCAL_IP}:9000)"
fi
fi
# Container Update Check
# - Skipped entirely when running unattended / non-interactive
# - Compose-managed containers: offered via Y/N → docker compose pull + up -d
# - Standalone containers: offered via Y/N → image pull only (no stop/rm)
if [ "$docker_installed" = true ] && ! _docker_is_noninteractive; then
msg_info "Checking for container updates"
local name image compose_workdir compose_service current_digest latest_digest
local compose_updates=()
local standalone_updates=()
while IFS= read -r line; do
name=$(echo "$line" | awk '{print $1}')
image=$(echo "$line" | awk '{print $2}')
# Portainer containers are handled by the dedicated block above
[[ "$name" == "portainer" || "$name" == "portainer_agent" ]] && continue
current_digest=$(docker inspect "$name" --format='{{.Image}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
docker pull "$image" >/dev/null 2>&1 || continue
latest_digest=$(docker inspect "$image" --format='{{.Id}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
[[ -z "$latest_digest" || "$current_digest" == "$latest_digest" ]] && continue
compose_workdir=$(docker inspect "$name" \
--format='{{index .Config.Labels "com.docker.compose.project.working_dir"}}' 2>/dev/null || true)
compose_service=$(docker inspect "$name" \
--format='{{index .Config.Labels "com.docker.compose.service"}}' 2>/dev/null || true)
if [[ -n "$compose_workdir" && -n "$compose_service" && -d "$compose_workdir" ]]; then
compose_updates+=("${name}|${image}|${compose_workdir}|${compose_service}")
else
standalone_updates+=("${name}|${image}")
fi
done < <(docker ps --format '{{.Names}} {{.Image}}')
# Stop spinner before any interactive prompt
stop_spinner
if [[ ${#compose_updates[@]} -eq 0 && ${#standalone_updates[@]} -eq 0 ]]; then
msg_ok "All containers are up-to-date"
else
local reply
for entry in "${compose_updates[@]}"; do
IFS='|' read -r name image compose_workdir compose_service <<<"$entry"
reply=""
if read -r -t 60 -p "${TAB3}Update ${name} (${image}) via Compose? <y/N> (auto-no in 60s): " reply </dev/tty; then
echo ""
else
echo ""
fi
if [[ "${reply,,}" =~ ^(y|yes)$ ]]; then
msg_info "Updating $name"
if (cd "$compose_workdir" && $STD docker compose pull "$compose_service" && $STD docker compose up -d "$compose_service"); then
msg_ok "Updated $name"
else
msg_warn "Could not update $name — try manually in $compose_workdir"
fi
fi
done
for entry in "${standalone_updates[@]}"; do
IFS='|' read -r name image <<<"$entry"
reply=""
if read -r -t 60 -p "${TAB3}Pull new image for ${name} (${image})? <y/N> (auto-no in 60s): " reply </dev/tty; then
echo ""
else
echo ""
fi
if [[ "${reply,,}" =~ ^(y|yes)$ ]]; then
msg_info "Pulling new image for $name"
if $STD docker pull "$image"; then
msg_ok "New image available for $name — recreate the container to apply the update"
else
msg_warn "Failed to pull image for $name"
fi
fi
done
fi
fi
msg_ok "Docker setup completed"
}
# ------------------------------------------------------------------------------
# Installs a .NET SDK or runtime.
#
# Description:
# - Debian and Ubuntu 22.04 install from packages.microsoft.com.
# - Ubuntu 24.04 and newer ship .NET in the distribution archive and Microsoft
# publishes no dotnet packages for them, so no extra repository is added.
# - .NET installs side by side, so an existing major version is left alone.
#
# Variables:
# DOTNET_VERSION - major version: 8, 9, 10 (default: 10). "8.0" also works.
# DOTNET_TYPE - sdk (default) | runtime | aspnetcore
#
# Notes:
# - Debian 13 carries 8, 9 and 10; Debian 12 and Ubuntu 22.04 additionally
# carry 6 and 7. Ubuntu 22.04 has no 10.
# - aspnetcore is the ASP.NET Core runtime that framework-dependent web apps
# need at runtime. Use sdk only when something has to be compiled.
# ------------------------------------------------------------------------------
setup_dotnet() {
local DOTNET_VERSION="${DOTNET_VERSION:-10}"
local DOTNET_TYPE="${DOTNET_TYPE:-sdk}"
local DISTRO_ID DISTRO_CODENAME DISTRO_VERSION PACKAGE_PREFIX
DOTNET_VERSION="${DOTNET_VERSION%%.*}.0"
case "$DOTNET_TYPE" in
sdk) PACKAGE_PREFIX="dotnet-sdk" ;;
runtime) PACKAGE_PREFIX="dotnet-runtime" ;;
aspnetcore) PACKAGE_PREFIX="aspnetcore-runtime" ;;
*)
msg_error "setup_dotnet: invalid DOTNET_TYPE '$DOTNET_TYPE' (expected sdk, runtime or aspnetcore)"
return 65
;;
esac
local DESIRED_PACKAGE="${PACKAGE_PREFIX}-${DOTNET_VERSION}"
DISTRO_ID=$(get_os_info id)
DISTRO_CODENAME=$(get_os_info codename)
DISTRO_VERSION=$(get_os_info version_id)
local USE_MS_REPO=1
if [[ "$DISTRO_ID" == "ubuntu" ]] && ((${DISTRO_VERSION%%.*} >= 24)); then
USE_MS_REPO=0
fi
if ((USE_MS_REPO)); then
local REPO_URL="https://packages.microsoft.com/${DISTRO_ID}/${DISTRO_VERSION}/prod"
prepare_repository_setup "microsoft-prod" || {
msg_error "Failed to prepare Microsoft repository"
return 100
}
if ! verify_repo_available "$REPO_URL" "$DISTRO_CODENAME"; then
msg_error "Microsoft does not publish a .NET feed for ${DISTRO_ID} ${DISTRO_VERSION} (${DISTRO_CODENAME})"
return 100
fi
setup_deb822_repo \
"microsoft-prod" \
"https://packages.microsoft.com/keys/microsoft-2025.asc" \
"$REPO_URL" \
"$DISTRO_CODENAME" \
"main" || return 100
fi
ensure_apt_working || return 100
if ! apt-cache show "$DESIRED_PACKAGE" &>/dev/null; then
msg_error "Package ${DESIRED_PACKAGE} is not available on ${DISTRO_ID} ${DISTRO_VERSION} (${DISTRO_CODENAME})"
return 100
fi
if dpkg-query -W -f '${Status}' "$DESIRED_PACKAGE" 2>/dev/null | grep -q "install ok installed"; then
msg_info "Update .NET ${DOTNET_TYPE} ${DOTNET_VERSION}"
upgrade_packages_with_retry "$DESIRED_PACKAGE" || {
msg_error "Failed to update ${DESIRED_PACKAGE}"
return 100
}
else
msg_info "Setup .NET ${DOTNET_TYPE} ${DOTNET_VERSION}"
install_packages_with_retry "$DESIRED_PACKAGE" || {
msg_error "Failed to install ${DESIRED_PACKAGE}"
return 100
}
fi
cache_installed_version "dotnet-${DOTNET_TYPE}" "$DOTNET_VERSION"
msg_ok "Setup .NET ${DOTNET_TYPE} ${DOTNET_VERSION}"
}
# ------------------------------------------------------------------------------
# Installs FFmpeg from the distribution repository, a prebuilt build, or source.
#
# Description:
# - Three acquisition strategies, selected via FFMPEG_TYPE:
# repo : distribution package (default) - fast, no build, LGPL
# github : prebuilt static build from BtbN/FFmpeg-Builds (daily, amd64+arm64)
# minimal : compile from source - x264, vpx, mp3 only
# medium : compile from source - adds subtitles, fonts, opus, vorbis
# full : compile from source - adds x265, dav1d, zlib, vaapi
# - "binary" is accepted as a deprecated alias for "github"
#
# Variables:
# FFMPEG_TYPE - repo (default) | github | minimal | medium | full
# FFMPEG_VERSION - source builds: git tag (default: latest stable)
# github builds: "latest" (master) or a release line, e.g. "n7.1"
# FFMPEG_LICENSE - github builds: gpl (default) | lgpl
#
# Notes:
# - Prefer "repo" unless the app needs a codec or version Debian does not ship.
# A source build takes 20+ minutes on a small container.
# - Source builds use --enable-gpl --enable-nonfree. Those binaries are NOT
# redistributable; they are only fine because they are built on the target
# host for its own use. Apps that require an LGPL FFmpeg must use "repo"
# or FFMPEG_LICENSE=lgpl.
# - repo installs to /usr/bin, github and source builds to /usr/local/bin.
# ------------------------------------------------------------------------------
setup_ffmpeg() {
local TMP_DIR
TMP_DIR=$(mktemp -d)
local GITHUB_REPO="FFmpeg/FFmpeg"
local VERSION="${FFMPEG_VERSION:-latest}"
local TYPE="${FFMPEG_TYPE:-repo}"
local LICENSE="${FFMPEG_LICENSE:-gpl}"
local BIN_PATH="/usr/local/bin/ffmpeg"
[[ "$TYPE" == "binary" ]] && TYPE="github"
# Get currently installed version
local INSTALLED_VERSION=""
if command -v ffmpeg &>/dev/null; then
INSTALLED_VERSION=$(ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}')
fi
msg_info "Setup FFmpeg ($TYPE)"
# Distribution package
if [[ "$TYPE" == "repo" ]]; then
if ! $STD apt install -y ffmpeg; then
msg_error "Failed to install FFmpeg from the distribution repository"
rm -rf "$TMP_DIR"
return 100
fi
local FINAL_VERSION
FINAL_VERSION=$(ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}')
rm -rf "$TMP_DIR"
cache_installed_version "ffmpeg" "$FINAL_VERSION"
[[ -n "$INSTALLED_VERSION" ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION" || msg_ok "Setup FFmpeg $FINAL_VERSION"
return 0
fi
# Prebuilt static build from BtbN/FFmpeg-Builds
if [[ "$TYPE" == "github" ]]; then
local ffmpeg_arch line
ffmpeg_arch=$(arch_resolve linux64 linuxarm64) || {
rm -rf "$TMP_DIR"
return 106
}
[[ "$VERSION" == "latest" || -z "$VERSION" ]] && line="master-latest" || line="${VERSION}-latest"
if ! CURL_TIMEOUT=300 curl_with_retry \
"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-${line}-${ffmpeg_arch}-${LICENSE}.tar.xz" \
"$TMP_DIR/ffmpeg.tar.xz"; then
msg_error "Failed to download FFmpeg build ffmpeg-${line}-${ffmpeg_arch}-${LICENSE}"
msg_error "Hint: check FFMPEG_VERSION/FFMPEG_LICENSE against the assets of https://github.com/BtbN/FFmpeg-Builds/releases/tag/latest"
rm -rf "$TMP_DIR"
return 250
fi
tar -xJf "$TMP_DIR/ffmpeg.tar.xz" -C "$TMP_DIR" || {
msg_error "Failed to extract FFmpeg archive"
rm -rf "$TMP_DIR"
return 251
}
if ! cp "$TMP_DIR/ffmpeg-"*/bin/ffmpeg "$TMP_DIR/ffmpeg-"*/bin/ffprobe /usr/local/bin/ 2>/dev/null; then
msg_error "Failed to install FFmpeg binaries"
rm -rf "$TMP_DIR"
return 150
fi
chmod +x "$BIN_PATH" /usr/local/bin/ffprobe
local FINAL_VERSION
FINAL_VERSION=$("$BIN_PATH" -version 2>/dev/null | head -n1 | awk '{print $3}')
rm -rf "$TMP_DIR"
cache_installed_version "ffmpeg" "$FINAL_VERSION"
ensure_usr_local_bin_persist
[[ -n "$INSTALLED_VERSION" ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION" || msg_ok "Setup FFmpeg $FINAL_VERSION"
return 0
fi
ensure_dependencies jq
# Auto-detect latest stable version if none specified
if [[ "$VERSION" == "latest" || -z "$VERSION" ]]; then
local ffmpeg_tags
ffmpeg_tags=$(curl -fsSL --max-time 15 "https://api.github.com/repos/${GITHUB_REPO}/tags" 2>/dev/null || echo "")
if [[ -z "$ffmpeg_tags" ]]; then
msg_error "Could not fetch FFmpeg versions from GitHub"
msg_error "Hint: use FFMPEG_TYPE=repo or FFMPEG_TYPE=github instead of a source build"
rm -rf "$TMP_DIR"
return 250
fi
VERSION=$(echo "$ffmpeg_tags" | jq -r '.[].name' 2>/dev/null |
grep -E '^n[0-9]+\.[0-9]+\.[0-9]+$' |
sort -V | tail -n1 || echo "")
fi
if [[ -z "$VERSION" ]]; then
msg_error "Could not determine an FFmpeg source version"
rm -rf "$TMP_DIR"
return 250
fi
# Dependency selection
local DEPS=(build-essential yasm nasm pkg-config)
case "$TYPE" in
minimal)
DEPS+=(libx264-dev libvpx-dev libmp3lame-dev)
;;
medium)
DEPS+=(libx264-dev libvpx-dev libmp3lame-dev libfreetype6-dev libass-dev libopus-dev libvorbis-dev)
;;
full)
DEPS+=(
libx264-dev libx265-dev libvpx-dev libmp3lame-dev
libfreetype6-dev libass-dev libopus-dev libvorbis-dev
libdav1d-dev zlib1g-dev libnuma-dev
libva-dev libdrm-dev
)
if apt-cache show libsvtav1enc-dev &>/dev/null; then
DEPS+=(libsvtav1enc-dev)
elif apt-cache show libsvtav1-dev &>/dev/null; then
DEPS+=(libsvtav1-dev)
fi
;;
*)
msg_error "Unknown FFMPEG_TYPE: $TYPE (expected repo, github, minimal, medium or full)"
rm -rf "$TMP_DIR"
return 65
;;
esac
ensure_dependencies "${DEPS[@]}"
if ! CURL_TIMEOUT=300 curl_with_retry "https://github.com/${GITHUB_REPO}/archive/refs/tags/${VERSION}.tar.gz" "$TMP_DIR/ffmpeg.tar.gz"; then
msg_error "Failed to download FFmpeg source ${VERSION}"
rm -rf "$TMP_DIR"
return 250
fi
tar -xzf "$TMP_DIR/ffmpeg.tar.gz" -C "$TMP_DIR" || {
msg_error "Failed to extract FFmpeg source"
rm -rf "$TMP_DIR"
return 251
}
local PREV_DIR="$PWD"
cd "$TMP_DIR/FFmpeg-"* || {
msg_error "Source extraction failed"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 251
}
local args=(
--enable-gpl
--enable-shared
--enable-nonfree
--disable-static
--enable-libx264
--enable-libvpx
--enable-libmp3lame
)
if [[ "$TYPE" != "minimal" ]]; then
args+=(--enable-libfreetype --enable-libass --enable-libopus --enable-libvorbis)
fi
if [[ "$TYPE" == "full" ]]; then
args+=(--enable-libx265 --enable-libdav1d --enable-zlib)
args+=(--enable-vaapi --enable-libdrm)
fi
if [[ ${#args[@]} -eq 0 ]]; then
msg_error "FFmpeg configure args array is empty"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 65
fi
$STD ./configure "${args[@]}" || {
msg_error "FFmpeg configure failed"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 150
}
$STD make -j"$(nproc)" || {
msg_error "FFmpeg compilation failed"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 150
}
$STD make install || {
msg_error "FFmpeg installation failed"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 150
}
echo "/usr/local/lib" >/etc/ld.so.conf.d/ffmpeg.conf
$STD ldconfig
ldconfig -p 2>/dev/null | grep libavdevice >/dev/null || {
msg_error "libavdevice not registered with dynamic linker"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 150
}
if ! command -v ffmpeg &>/dev/null; then
msg_error "FFmpeg installation failed"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 150
fi
local FINAL_VERSION
FINAL_VERSION=$(ffmpeg -version 2>/dev/null | head -n1 | awk '{print $3}')
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
cache_installed_version "ffmpeg" "$FINAL_VERSION"
ensure_usr_local_bin_persist
[[ -n "$INSTALLED_VERSION" ]] && msg_ok "Upgrade FFmpeg $INSTALLED_VERSION → $FINAL_VERSION" || msg_ok "Setup FFmpeg $FINAL_VERSION"
}
# ------------------------------------------------------------------------------
# Installs Go (Golang) from official tarball.
#
# Description:
# - Determines system architecture
# - Downloads latest version if GO_VERSION not set
#
# Variables:
# GO_VERSION - Version to install (e.g. 1.22.2 or latest)
# ------------------------------------------------------------------------------
setup_go() {
local ARCH
case "$(uname -m)" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
*)
msg_error "Unsupported architecture: $(uname -m)"
return 236
;;
esac
# Resolve "latest" version
local GO_VERSION="${GO_VERSION:-latest}"
if [[ "$GO_VERSION" == "latest" ]]; then
local go_version_tmp
go_version_tmp=$(curl_with_retry "https://go.dev/VERSION?m=text" "-" 2>/dev/null | head -n1 | sed 's/^go//') || true
if [[ -z "$go_version_tmp" ]]; then
msg_error "Could not determine latest Go version"
return 250
fi
GO_VERSION="$go_version_tmp"
fi
local GO_BIN="/usr/local/bin/go"
local GO_INSTALL_DIR="/usr/local/go"
# Get currently installed version
local CURRENT_VERSION=""
if [[ -x "$GO_BIN" ]]; then
CURRENT_VERSION=$("$GO_BIN" version 2>/dev/null | awk '{print $3}' | sed 's/go//')
fi
# Scenario 1: Already at target version
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$GO_VERSION" ]]; then
cache_installed_version "go" "$GO_VERSION"
return 0
fi
# Scenario 2: Different version or not installed
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$GO_VERSION" ]]; then
msg_info "Upgrade Go from $CURRENT_VERSION to $GO_VERSION"
remove_old_tool_version "go"
else
msg_info "Setup Go $GO_VERSION"
fi
local TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz"
local URL="https://go.dev/dl/${TARBALL}"
local TMP_TAR=$(mktemp)
if ! CURL_TIMEOUT=300 curl_with_retry "$URL" "$TMP_TAR"; then
msg_error "Failed to download Go $GO_VERSION"
msg_error "Hint: Check connectivity to go.dev/dl — URL: $URL"
rm -f "$TMP_TAR"
return 250
fi
$STD tar -C /usr/local -xzf "$TMP_TAR" || {
msg_error "Failed to extract Go tarball"
rm -f "$TMP_TAR"
return 251
}
ln -sf /usr/local/go/bin/go /usr/local/bin/go
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
rm -f "$TMP_TAR"
cache_installed_version "go" "$GO_VERSION"
ensure_usr_local_bin_persist
msg_ok "Setup Go $GO_VERSION"
}
# ------------------------------------------------------------------------------
# Installs or updates Ghostscript (gs) from source.
#
# Description:
# - Fetches latest release
# - Builds and installs system-wide
# ------------------------------------------------------------------------------
setup_gs() {
local TMP_DIR=$(mktemp -d)
local PREV_DIR="$PWD"
local CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0")
ensure_dependencies jq
local LATEST_VERSION
# Tags are "gs10.05.1" format — fetch without stripping "v", then strip "gs"
LATEST_VERSION=$(get_latest_github_release "ArtifexSoftware/ghostpdl-downloads" "false") || {
msg_warn "Cannot fetch latest Ghostscript version from GitHub API"
if command -v gs &>/dev/null; then
gs --version | head -n1
cache_installed_version "ghostscript" "$CURRENT_VERSION"
return 0
fi
msg_error "Cannot determine Ghostscript version and no existing installation found"
return 250
}
LATEST_VERSION="${LATEST_VERSION#gs}"
local LATEST_VERSION_DOTTED=$(echo "$LATEST_VERSION" | sed 's/\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{1\}\)/\1.\2.\3/')
if [[ -z "$LATEST_VERSION" || -z "$LATEST_VERSION_DOTTED" ]]; then
msg_warn "Could not determine latest Ghostscript version from GitHub - checking system"
# Fallback: try to use system version or return error
if [[ "$CURRENT_VERSION" == "0" ]]; then
msg_error "Ghostscript not installed and cannot determine latest version"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 250
fi
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 0
fi
# Scenario 1: Already at latest version
if [[ -n "$LATEST_VERSION_DOTTED" ]] && dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED" 2>/dev/null; then
cache_installed_version "ghostscript" "$LATEST_VERSION_DOTTED"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 0
fi
# Scenario 2: New install or upgrade
if [[ "$CURRENT_VERSION" != "0" && "$CURRENT_VERSION" != "$LATEST_VERSION_DOTTED" ]]; then
msg_info "Upgrade Ghostscript from $CURRENT_VERSION to $LATEST_VERSION_DOTTED"
else
msg_info "Setup Ghostscript $LATEST_VERSION_DOTTED"
fi
if ! CURL_TIMEOUT=180 curl_with_retry "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" "$TMP_DIR/ghostscript.tar.gz"; then
msg_error "Failed to download Ghostscript"
msg_error "Hint: Check connectivity to github.com/ArtifexSoftware — may be a GitHub rate-limit, set GITHUB_TOKEN"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 250
fi
if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then
msg_error "Failed to extract Ghostscript archive"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 251
fi
# Verify directory exists before cd
if [[ ! -d "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" ]]; then
msg_error "Ghostscript source directory not found: $TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 252
fi
cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || {
msg_error "Failed to enter Ghostscript source directory"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 252
}
ensure_dependencies build-essential libpng-dev zlib1g-dev
$STD ./configure || {
msg_error "Ghostscript configure failed"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 150
}
$STD make -j"$(nproc)" || {
msg_error "Ghostscript compilation failed"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 150
}
$STD make install || {
msg_error "Ghostscript installation failed"
cd "$PREV_DIR" 2>/dev/null || cd /
rm -rf "$TMP_DIR"
return 150
}