mjolnir-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. mjolnir_cli/__init__.py +8 -0
  2. mjolnir_cli/__main__.py +4 -0
  3. mjolnir_cli/accuracy.py +333 -0
  4. mjolnir_cli/autoresearch.py +539 -0
  5. mjolnir_cli/benchy.py +475 -0
  6. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4/config.yaml +78 -0
  7. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_DFLASH2/config.yaml +103 -0
  8. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_DSPARK/config.yaml +102 -0
  9. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_FA2/config.yaml +84 -0
  10. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_FA4hd256/config.yaml +104 -0
  11. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR/config.yaml +86 -0
  12. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3/config.yaml +93 -0
  13. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_DFLASH2/config.yaml +123 -0
  14. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_DSPARK/config.yaml +124 -0
  15. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8/.env +2 -0
  16. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8/config.yaml +98 -0
  17. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT32K/.env +4 -0
  18. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT32K/config.yaml +98 -0
  19. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT64K/.env +4 -0
  20. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_LMH8_VT64K/config.yaml +102 -0
  21. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_VT32K/.env +3 -0
  22. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/NVFP4_MJOLNIR_E2M3_VT32K/config.yaml +98 -0
  23. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/draft_vocab.json +1 -0
  24. mjolnir_cli/bundled/configs/Qwen/Qwen3.8-27B/kv_scales.json +35197 -0
  25. mjolnir_cli/bundled/configs/Qwen/Qwen3.8_chat_template.jinja +456 -0
  26. mjolnir_cli/bundled/docker/litellm/docker-compose.yaml +113 -0
  27. mjolnir_cli/bundled/docker/litellm/litellm.env.example +51 -0
  28. mjolnir_cli/bundled/docker/litellm/litellm_config.template.example.yaml +192 -0
  29. mjolnir_cli/bundled/docker/perses/README.md +89 -0
  30. mjolnir_cli/bundled/docker/perses/dashboards/global-datasource.yaml +17 -0
  31. mjolnir_cli/bundled/docker/perses/dashboards/performance_statistics.yaml +771 -0
  32. mjolnir_cli/bundled/docker/perses/dashboards/project.yaml +13 -0
  33. mjolnir_cli/bundled/docker/perses/dashboards/query_statistics.yaml +399 -0
  34. mjolnir_cli/bundled/docker/perses/docker-compose.yaml +51 -0
  35. mjolnir_cli/bundled/docker/perses/perses.yml +16 -0
  36. mjolnir_cli/bundled/docker/perses/prometheus.template.yaml +20 -0
  37. mjolnir_cli/bundled/scripts/hw/setup-thor.sh +1287 -0
  38. mjolnir_cli/cachectl.py +146 -0
  39. mjolnir_cli/charts.py +1687 -0
  40. mjolnir_cli/cli.py +1480 -0
  41. mjolnir_cli/clidoc.py +170 -0
  42. mjolnir_cli/config.py +346 -0
  43. mjolnir_cli/dockerctl.py +533 -0
  44. mjolnir_cli/drivers/fa2_driver.py +89 -0
  45. mjolnir_cli/drivers/fi_driver.py +134 -0
  46. mjolnir_cli/e2e.py +302 -0
  47. mjolnir_cli/fonts/InterDisplay-Bold.ttf +0 -0
  48. mjolnir_cli/fonts/InterDisplay-Medium.ttf +0 -0
  49. mjolnir_cli/fonts/InterDisplay-Regular.ttf +0 -0
  50. mjolnir_cli/fonts/InterDisplay-SemiBold.ttf +0 -0
  51. mjolnir_cli/fonts/OFL.txt +92 -0
  52. mjolnir_cli/gate.py +233 -0
  53. mjolnir_cli/history.py +159 -0
  54. mjolnir_cli/interactive.py +383 -0
  55. mjolnir_cli/legs.py +109 -0
  56. mjolnir_cli/litellm.py +254 -0
  57. mjolnir_cli/perses.py +214 -0
  58. mjolnir_cli/picker.py +70 -0
  59. mjolnir_cli/readme.py +1020 -0
  60. mjolnir_cli/stack.py +340 -0
  61. mjolnir_cli/tasks.py +122 -0
  62. mjolnir_cli/theme.py +281 -0
  63. mjolnir_cli/thor.py +108 -0
  64. mjolnir_cli/verify/__init__.py +2 -0
  65. mjolnir_cli/verify/cli.py +816 -0
  66. mjolnir_cli/verify/docker.py +334 -0
  67. mjolnir_cli/verify/kvcache.py +180 -0
  68. mjolnir_cli/verify/legs.py +134 -0
  69. mjolnir_cli/verify/ncu.py +141 -0
  70. mjolnir_cli/verify/profile.py +312 -0
  71. mjolnir_cli/verify/report.py +177 -0
  72. mjolnir_cli/verify/results.py +157 -0
  73. mjolnir_cli/verify/spec.py +279 -0
  74. mjolnir_cli/verify/stages.py +817 -0
  75. mjolnir_cli-0.2.0.dist-info/METADATA +87 -0
  76. mjolnir_cli-0.2.0.dist-info/RECORD +80 -0
  77. mjolnir_cli-0.2.0.dist-info/WHEEL +4 -0
  78. mjolnir_cli-0.2.0.dist-info/entry_points.txt +2 -0
  79. mjolnir_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
  80. mjolnir_cli-0.2.0.dist-info/licenses/NOTICE +64 -0
@@ -0,0 +1,1287 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # setup-thor.sh — Jetson AGX Thor (sm_110a) host pre-configuration.
4
+ #
5
+ # Ported from the ~/thor repo's scripts/setup.sh + utils.sh, made
6
+ # self-contained and Thor-only (SoM=thor, 128 GB, NVIDIA carrier — the
7
+ # constants setup.sh hardcoded): no external ROOT/scripts dependency, no
8
+ # CI branches. This is for a physical box and needs sudo. Every step is
9
+ # idempotent — re-running where already configured is a no-op.
10
+ #
11
+ # The fan step also installs the two tuned performance fan profiles
12
+ # (`recommended` + `max`, per-profile gated, with the .bck rollback point)
13
+ # and selects the default — see the FAN section below.
14
+ #
15
+ # Preferred entry point: `mjolnir hw setup` (maps CLI flags to the options
16
+ # below):
17
+ #
18
+ # -y, --yes don't prompt before starting
19
+ # --no-upgrade skip apt update + full-upgrade (default: run)
20
+ # --keep-gui don't switch the boot target to multi-user
21
+ # --fan-profile <p> recommended (default) | max | cool | quiet
22
+ # --swap-size <GB> swap file size (default: 32)
23
+ # --skip <a,b,c> skip steps: gui,upgrade,pip,docker,jtop,
24
+ # memory,fan,host,clocks,power
25
+ # --reboot reboot at the end if a step required one
26
+ # --dry-run print the plan, change nothing
27
+
28
+ set -euo pipefail
29
+
30
+ # ── Thor constants (from setup.sh's hardcoded block) ─────────────────────────
31
+ SOC_TYPE="thor"
32
+ SOC_MEMORY=128
33
+ IS_NVIDIA_CARRIER=true
34
+
35
+ REBOOT_REQUIRED=0
36
+
37
+ ASSUME_YES=0
38
+ UPGRADE=1
39
+ KEEP_GUI=0
40
+ FAN_PROFILE="recommended"
41
+ SWAP_SIZE=32
42
+ SKIP=""
43
+ DO_REBOOT=0
44
+ DRY_RUN=0
45
+
46
+ usage() {
47
+ grep '^# ' "$0" | sed 's/^# //'
48
+ }
49
+
50
+ while [[ $# -gt 0 ]]; do
51
+ case "$1" in
52
+ -y|--yes) ASSUME_YES=1; shift ;;
53
+ --no-upgrade) UPGRADE=0; shift ;;
54
+ --keep-gui) KEEP_GUI=1; shift ;;
55
+ --fan-profile) FAN_PROFILE="${2:?}"; shift 2 ;;
56
+ --swap-size) SWAP_SIZE="${2:?}"; shift 2 ;;
57
+ --skip) SKIP="${2:?}"; shift 2 ;;
58
+ --reboot) DO_REBOOT=1; shift ;;
59
+ --dry-run) DRY_RUN=1; shift ;;
60
+ -h|--help) usage; exit 0 ;;
61
+ *) echo "❌ unknown option: $1" >&2; usage >&2; exit 2 ;;
62
+ esac
63
+ done
64
+
65
+ # ================================================================================
66
+ # UTILITIES
67
+ # ================================================================================
68
+
69
+ is_true() {
70
+ [[ "${1,,}" =~ ^(true|1|yes|enabled|y)$ ]]
71
+ }
72
+
73
+ file_exists() {
74
+ local file=${1:-}
75
+ [[ -n "$file" && -f "$file" ]]
76
+ }
77
+
78
+ is_command_available() {
79
+ local command=$1
80
+ if command -v "${command}" &> /dev/null; then
81
+ return 0
82
+ else
83
+ return 1
84
+ fi
85
+ }
86
+
87
+ is_user_in_group() {
88
+ getent group "$1" | grep -qw "$USER"
89
+ }
90
+
91
+ # Convert size string to bytes (e.g., "4G" to bytes)
92
+ convert_to_bytes() {
93
+ local size=$1
94
+ local value=${size%[GM]}
95
+ local unit=${size#$value}
96
+
97
+ case $unit in
98
+ G)
99
+ echo $((value * 1024 * 1024 * 1024))
100
+ ;;
101
+ M)
102
+ echo $((value * 1024 * 1024))
103
+ ;;
104
+ *)
105
+ echo "Error: Invalid size unit. Use M for MB or G for GB"
106
+ exit 1
107
+ ;;
108
+ esac
109
+ }
110
+
111
+ is_skipped() {
112
+ case ",${SKIP}," in *",${1},"*) return 0 ;; esac
113
+ return 1
114
+ }
115
+
116
+ ask_yes_no() {
117
+ while true; do
118
+ read -p "❔ ${1} (y/n): " yn
119
+ case $yn in
120
+ [Yy]* ) return 0;;
121
+ [Nn]* ) return 1;;
122
+ * ) echo "Please answer Y or y for YES or N or n for NO.";;
123
+ esac
124
+ done
125
+ }
126
+
127
+ ask_should_proceed() {
128
+ echo
129
+ while true; do
130
+ read -p "❔ Do you want to continue? (Press ENTER for Yes, 'q' to Quit) " input
131
+ if [ -z "$input" ] || is_true "$input"; then
132
+ echo "✅ Continuing..."
133
+ echo
134
+ return 0
135
+ elif [[ "$input" == "q" ]]; then
136
+ echo "❌ Exiting..."
137
+ exit 0
138
+ else
139
+ echo "❌ Invalid input: ${input}. Please press ENTER to continue or 'q' to quit."
140
+ fi
141
+ done
142
+ }
143
+
144
+ check_systemd() {
145
+ if ! is_command_available systemctl; then
146
+ echo "Error: This script requires systemd"
147
+ exit 1
148
+ fi
149
+ }
150
+
151
+ systemctl_stop_service() {
152
+ check_systemd
153
+ sudo systemctl stop "$1"
154
+ }
155
+
156
+ systemctl_enable_service() {
157
+ check_systemd
158
+ sudo systemctl enable --now "$1"
159
+ }
160
+
161
+ systemctl_start_service() {
162
+ check_systemd
163
+ sudo systemctl start "$1"
164
+ }
165
+
166
+ systemctl_restart_service() {
167
+ check_systemd
168
+ sudo systemctl restart "$1"
169
+ }
170
+
171
+ systemctl_service_exists() {
172
+ check_systemd
173
+
174
+ # status exit code 4 = unit not found
175
+ systemctl status "$1" &>/dev/null
176
+ [[ $? -ne 4 ]]
177
+ }
178
+
179
+ # Return 0 if the unit is active (running), 1 otherwise
180
+ systemctl_service_is_active() {
181
+ check_systemd
182
+ systemctl is-active --quiet "$1"
183
+ }
184
+
185
+ # Return 0 if the unit is enabled, 1 otherwise
186
+ systemctl_service_is_enabled() {
187
+ check_systemd
188
+ systemctl is-enabled --quiet "$1"
189
+ }
190
+
191
+ systemctl_disable_service() {
192
+ if ! systemctl_service_exists "$1"; then
193
+ return 0
194
+ fi
195
+
196
+ if systemctl_service_is_active "$1"; then
197
+ echo "ℹ️ Stopping the $1 service..."
198
+ systemctl_stop_service "$1"
199
+ fi
200
+
201
+ if systemctl_service_is_enabled "$1"; then
202
+ echo "ℹ️ Disabling the $1 service..."
203
+ sudo systemctl disable "$1"
204
+ fi
205
+ }
206
+
207
+ ensure_installed() {
208
+ # Usage: ensure_installed <command> <package>
209
+ local cmd="$1" pkg="${2:-$1}"
210
+
211
+ if ! command -v "$cmd" >/dev/null 2>&1; then
212
+ echo "📦 Installing $pkg..."
213
+ sudo apt update
214
+ sudo apt install -y "$pkg"
215
+ else
216
+ echo "✅ $pkg is already installed at $(command -v "$cmd")"
217
+ fi
218
+ }
219
+
220
+ l4t_root_device() {
221
+ local root_device
222
+ root_device=$(findmnt -n -o SOURCE /)
223
+
224
+ if [[ $root_device == *"nvme"* ]]; then
225
+ echo "nvme"
226
+ elif [[ $root_device == *"mmcblk"* ]]; then
227
+ # eMMC devices have mmcblk0boot0 and mmcblk0boot1 partitions
228
+ if [[ -b /dev/mmcblk0boot0 ]] && [[ -b /dev/mmcblk0boot1 ]]; then
229
+ echo "emmc"
230
+ else
231
+ echo "sdcard"
232
+ fi
233
+ elif [[ $root_device == *"/dev/sd"* ]]; then
234
+ echo "usb_sata"
235
+ else
236
+ echo "unknown"
237
+ fi
238
+ }
239
+
240
+ is_l4t_installed_on_sdcard() { [[ "$(l4t_root_device)" == "sdcard" ]]; }
241
+
242
+ # ================================================================================
243
+ # SYSTEM / HOST
244
+ # ================================================================================
245
+
246
+ upgrade_system() {
247
+ if is_true "$UPGRADE"; then
248
+ echo "📥 Updating package lists..."
249
+ sudo apt update &> /dev/null
250
+
251
+ echo "📥 Upgrading system..."
252
+ sudo apt full-upgrade -y &> /dev/null
253
+ echo "✅ System upgraded."
254
+
255
+ ensure_installed nvidia-jetpack &> /dev/null
256
+ else
257
+ echo "ℹ️ Skipping system upgrade (--no-upgrade)."
258
+ fi
259
+
260
+ ensure_installed curl curl
261
+ ensure_installed wget wget
262
+ }
263
+
264
+ # ================================================================================
265
+ # INSTALLERS
266
+ # ================================================================================
267
+
268
+ install_pip() {
269
+ # If pip already on PATH, we're done
270
+ if is_command_available pip; then
271
+ echo "✅ pip is already installed."
272
+ return
273
+ fi
274
+
275
+ echo "ℹ️ Installing pip and supporting tools..."
276
+ if ! sudo apt install -y python3-pip python3-setuptools python3-pkg-resources; then
277
+ echo "⚠️ Attempting to fix broken packages..."
278
+ sudo apt --fix-broken install -y
279
+ if ! sudo apt install -y python3-pip python3-setuptools python3-pkg-resources; then
280
+ echo "🚨 Falling back to get-pip.py bootstrap..."
281
+
282
+ ensure_installed curl curl
283
+
284
+ curl -sSL https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
285
+ sudo python3 /tmp/get-pip.py
286
+ rm -f /tmp/get-pip.py
287
+ fi
288
+ fi
289
+
290
+ echo "ℹ️ Checking for pip3 in standard paths..."
291
+ if ! is_command_available pip3; then
292
+ echo "⚠️ pip3 not found in PATH — checking ~/.local/bin..."
293
+ export PATH="$HOME/.local/bin:$PATH"
294
+ if ! is_command_available pip3; then
295
+ echo "❌ pip3 still not found. Check your installation manually."
296
+ return 1
297
+ else
298
+ echo "✅ Found pip3 in ~/.local/bin — added to PATH for this session."
299
+ fi
300
+ fi
301
+
302
+ echo "ℹ️ Verifying pip3 version..."
303
+ pip3 --version
304
+
305
+ echo "ℹ️ Upgrading pip and tools..."
306
+ python3 -m pip install --upgrade --user pip setuptools wheel
307
+
308
+ echo "ℹ️ Ensuring ~/.local/bin is in PATH..."
309
+ if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
310
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
311
+ echo '✅ Added ~/.local/bin to ~/.bashrc for future sessions.'
312
+ fi
313
+
314
+ echo "✅ pip3 installation complete and usable in this session."
315
+ }
316
+
317
+ install_jtop() {
318
+ # https://github.com/rbonghi/jetson_stats
319
+ if ! command -v jtop &> /dev/null; then
320
+ echo "📦 Installing jtop..."
321
+ pip3 install -U "setuptools<71.0.0"
322
+ sudo env "PATH=$HOME/.local/bin:$PATH" pip3 install -U jetson-stats
323
+ systemctl_restart_service jtop.service
324
+ echo "✅ jtop was installed successfully."
325
+ else
326
+ echo "✅ jtop is already installed."
327
+ fi
328
+ }
329
+
330
+ # ================================================================================
331
+ # DOCKER
332
+ # ================================================================================
333
+
334
+ ensure_docker_repo() {
335
+ local docker_keyring_path="/etc/apt/keyrings/docker.asc"
336
+
337
+ if ! file_exists "${docker_keyring_path}"; then
338
+ echo "🔑 Adding Docker GPG key and repository..."
339
+
340
+ sudo apt update
341
+ sudo apt install ca-certificates curl
342
+ sudo install -m 0755 -d /etc/apt/keyrings
343
+ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
344
+ sudo chmod a+r /etc/apt/keyrings/docker.asc
345
+
346
+ # Add the repository to Apt sources:
347
+ echo \
348
+ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
349
+ $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
350
+ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
351
+ sudo apt update
352
+ fi
353
+ }
354
+
355
+ install_docker() {
356
+ ensure_docker_repo
357
+
358
+ if ! is_command_available docker; then
359
+ echo "📦 Installing docker..."
360
+
361
+ sudo apt update
362
+ sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin nvidia-container-toolkit
363
+
364
+ echo "✅ docker was installed successfully."
365
+ else
366
+ echo "✅ docker is already installed."
367
+ fi
368
+
369
+ if ! docker compose version &> /dev/null; then
370
+ echo "📦 Installing docker-compose..."
371
+ sudo apt install -y docker-compose-plugin
372
+ echo "✅ docker-compose was installed successfully."
373
+ else
374
+ echo "✅ docker-compose is already installed."
375
+ fi
376
+ }
377
+
378
+ # Add user to docker group
379
+ setup_docker_group() {
380
+ # Check if user is in docker group
381
+ if is_user_in_group docker; then
382
+ echo "✅ User '$USER' is already in the docker group."
383
+ else
384
+ echo "Adding $USER to the docker group..."
385
+ sudo usermod -aG docker "$USER"
386
+
387
+ # Group membership takes effect at next login; the live socket may
388
+ # stay unwritable until then.
389
+ if [[ -S /var/run/docker.sock && ! -w /var/run/docker.sock ]]; then
390
+ echo "ℹ️ The docker socket stays unwritable until you log in again."
391
+ fi
392
+ fi
393
+ }
394
+
395
+ # Function to check Docker runtime configuration
396
+ check_docker_runtime() {
397
+ local daemon_config="/etc/docker/daemon.json"
398
+
399
+ if ! is_command_available docker; then
400
+ return 1
401
+ fi
402
+
403
+ if ! file_exists $daemon_config; then
404
+ return 1
405
+ fi
406
+
407
+ if grep -q '"default-runtime": "nvidia"' $daemon_config 2>/dev/null; then
408
+ return 0
409
+ else
410
+ return 1
411
+ fi
412
+ }
413
+
414
+ # Configure Docker with NVIDIA runtime as default
415
+ configure_docker_runtime() {
416
+ local daemon_config="/etc/docker/daemon.json"
417
+
418
+ # THOR:
419
+ # -----
420
+ # sudo nvidia-ctk runtime configure --runtime=docker
421
+ # to configure it to following state:
422
+ # {
423
+ # "runtimes": {
424
+ # "nvidia": {
425
+ # "args": [],
426
+ # "path": "nvidia-container-runtime"
427
+ # }
428
+ # }
429
+ # }
430
+ # and always use --runtime=nvidia when running docker docntainers with GPU
431
+ # -----
432
+
433
+ if check_docker_runtime; then
434
+ echo "✅ NVIDIA runtime is already set as default in Docker: ${daemon_config}"
435
+ else
436
+ echo "❌ NVIDIA runtime is NOT set as default - configuring now..."
437
+
438
+ # Check if jq is installed
439
+ ensure_installed jq jq
440
+
441
+ # Configure NVIDIA runtime as default
442
+ local tmp
443
+ tmp=$(mktemp)
444
+ if file_exists $daemon_config; then
445
+ sudo jq '.
446
+ | .runtimes += {
447
+ "nvidia": {
448
+ "path": "nvidia-container-runtime",
449
+ "runtimeArgs": []
450
+ }
451
+ }
452
+ | .["default-runtime"] = "nvidia"
453
+ ' "$daemon_config" > "$tmp"
454
+ sudo mv "$tmp" "$daemon_config"
455
+ else
456
+ cat > "$tmp" <<'EOF'
457
+ {
458
+ "runtimes": {
459
+ "nvidia": {
460
+ "path": "nvidia-container-runtime",
461
+ "runtimeArgs": []
462
+ }
463
+ },
464
+ "default-runtime": "nvidia"
465
+ }
466
+ EOF
467
+ sudo mv "$tmp" "$daemon_config"
468
+ fi
469
+
470
+ # The tmp file is user-owned; daemon.json must be root:root 644.
471
+ sudo chown root:root "$daemon_config"
472
+ sudo chmod 644 "$daemon_config"
473
+
474
+ # Restart Docker to apply changes
475
+ echo "ℹ️ Restarting Docker service to apply changes..."
476
+ systemctl_restart_service docker
477
+ echo "✅ NVIDIA runtime is now set as default."
478
+ fi
479
+ }
480
+
481
+ ensure_docker_running_and_enabled() {
482
+ # 1. Does the unit exist?
483
+ if ! systemctl_service_exists docker; then
484
+ echo "❌ Docker service is not installed. Please install Docker first."
485
+ return 1
486
+ fi
487
+
488
+ # 2. Start it if it's not running
489
+ if ! systemctl_service_is_active docker; then
490
+ echo "▶️ Starting Docker service..."
491
+ systemctl_start_service docker
492
+
493
+ if systemctl_service_is_active docker; then
494
+ echo "✅ Docker service started."
495
+ else
496
+ echo "❌ Failed to start Docker service."
497
+ return 1
498
+ fi
499
+ else
500
+ echo "✅ Docker service is already running."
501
+ fi
502
+
503
+ # 3. Enable it at boot if it's not enabled
504
+ if ! systemctl_service_is_enabled docker; then
505
+ echo "⚙️ Enabling Docker service at boot..."
506
+ systemctl_enable_service docker
507
+
508
+ if systemctl_service_is_enabled docker; then
509
+ echo "✅ Docker service is now enabled at boot."
510
+ else
511
+ echo "❌ Failed to enable Docker service at boot."
512
+ return 1
513
+ fi
514
+ else
515
+ echo "✅ Docker service is already enabled at boot."
516
+ fi
517
+ }
518
+
519
+ # ================================================================================
520
+ # SWAP
521
+ # ================================================================================
522
+
523
+ check_swap_exists() {
524
+ local swap_file="${1:-}"
525
+
526
+ # non-empty and regular file
527
+ file_exists "${swap_file}" || return 1
528
+
529
+ # check if it's active
530
+ if swapon --noheadings --raw --show=NAME | grep -Fqx "${swap_file}"; then
531
+ return 0
532
+ else
533
+ return 1
534
+ fi
535
+ }
536
+
537
+ # Check if swap file exists and is active
538
+ check_swap_file_status() {
539
+ local swap_file="${1:-}"
540
+
541
+ if file_exists "$swap_file"; then
542
+ if check_swap_exists "$swap_file"; then
543
+ echo "active"
544
+ else
545
+ echo "inactive"
546
+ fi
547
+ else
548
+ echo "missing"
549
+ fi
550
+ }
551
+
552
+ # Helper function for robust swap file cleanup
553
+ cleanup_swap_file() {
554
+ local swap_file="$1"
555
+
556
+ [[ -z "${swap_file}" ]] && return 0
557
+
558
+ sync 2>/dev/null || true
559
+ sudo swapoff "${swap_file}" 2>/dev/null || true
560
+ sudo rm -f "${swap_file}" 2>/dev/null || true
561
+ }
562
+
563
+ # Create and enable swap file with safety checks
564
+ setup_swap_file() {
565
+ local swap_size=$SWAP_SIZE
566
+ local swap_file_path="/mnt/${swap_size}GB.swap"
567
+ local swap_state
568
+ local size_bytes
569
+ swap_state=$(check_swap_file_status "${swap_file_path}")
570
+ size_bytes=$(convert_to_bytes "${swap_size}G")
571
+
572
+ # Validate inputs
573
+ if [[ -z "${swap_file_path}" ]]; then
574
+ echo "❌ Error: swap_file_path is not set"
575
+ return 1
576
+ fi
577
+
578
+ if [[ -z "${size_bytes}" ]] || [[ "${size_bytes}" -le 0 ]]; then
579
+ echo "❌ Error: Invalid swap size: ${swap_size}"
580
+ return 1
581
+ fi
582
+
583
+ # Validate swap file path (should be absolute and in safe location)
584
+ if [[ "${swap_file_path}" != /* ]]; then
585
+ echo "❌ Error: swap_file_path must be absolute: ${swap_file_path}"
586
+ return 1
587
+ fi
588
+
589
+ # Ensure path is in a safe location (not root, boot, etc.)
590
+ case "${swap_file_path}" in
591
+ /boot/*|/proc/*|/sys/*|/dev/*|/run/*)
592
+ echo "❌ Error: Unsafe swap file location: ${swap_file_path}"
593
+ return 1
594
+ ;;
595
+ esac
596
+
597
+ # Get directory and check if it exists
598
+ local swap_dir=$(dirname "${swap_file_path}")
599
+ if [[ ! -d "${swap_dir}" ]]; then
600
+ echo "❌ Error: Directory does not exist: ${swap_dir}"
601
+ return 1
602
+ fi
603
+
604
+ # Check write permissions on directory
605
+ if [[ ! -w "${swap_dir}" ]] && ! sudo test -w "${swap_dir}"; then
606
+ echo "❌ Error: No write permission for directory: ${swap_dir}"
607
+ return 1
608
+ fi
609
+
610
+ # Check available space (add 10% buffer)
611
+ local available_space
612
+ available_space=$(df --output=avail -B1 "${swap_dir}" | tail -n1)
613
+ if [[ "${size_bytes}" -gt "${available_space}" ]]; then
614
+ echo "❌ Error: Insufficient disk space"
615
+ echo " Requested: $(numfmt --to=iec "${size_bytes}" 2>/dev/null || echo "${size_bytes} bytes")"
616
+ echo " Available: $(numfmt --to=iec "${available_space}" 2>/dev/null || echo "${available_space} bytes")"
617
+ return 1
618
+ fi
619
+
620
+ # If swap file exists, disable and remove it first
621
+ if [ "${swap_state}" != "missing" ]; then
622
+ cleanup_swap_file "${swap_file_path}"
623
+ # Remove from fstab if present
624
+ sudo sed -i "\\#^${swap_file_path}#d" /etc/fstab
625
+ fi
626
+
627
+ # Create swap only when not installed on sdcard
628
+ if ! is_l4t_installed_on_sdcard; then
629
+ # Calculate count more safely
630
+ local count_mb=$((size_bytes/1024/1024))
631
+ if [[ $((count_mb * 1024 * 1024)) -ne "${size_bytes}" ]]; then
632
+ echo "❌ Error: Size calculation overflow or precision loss"
633
+ return 1
634
+ fi
635
+
636
+ # Use fallocate if available (faster and safer than dd)
637
+ if command -v fallocate >/dev/null 2>&1; then
638
+ if ! sudo fallocate -l "${size_bytes}" "${swap_file_path}"; then
639
+ echo "❌ Error: Failed to create swap file with fallocate"
640
+ return 1
641
+ fi
642
+ else
643
+ # Fallback to dd with additional safety
644
+ if ! sudo dd if=/dev/zero of="${swap_file_path}" bs=1M count="${count_mb}" status=progress conv=fsync 2>/dev/null; then
645
+ echo "❌ Error: Failed to create swap file with dd"
646
+ # Clean up partial file
647
+ cleanup_swap_file "${swap_file_path}"
648
+ return 1
649
+ fi
650
+ fi
651
+
652
+ # Set proper permissions
653
+ if ! sudo chmod 600 "${swap_file_path}"; then
654
+ echo "❌ Error: Failed to set swap file permissions"
655
+ cleanup_swap_file "${swap_file_path}"
656
+ return 1
657
+ fi
658
+
659
+ # Initialize swap
660
+ if ! sudo mkswap "${swap_file_path}" >/dev/null; then
661
+ echo "❌ Error: Failed to initialize swap file"
662
+ cleanup_swap_file "${swap_file_path}"
663
+ return 1
664
+ fi
665
+
666
+ # Enable swap
667
+ if ! sudo swapon "${swap_file_path}"; then
668
+ echo "❌ Error: Failed to enable swap file"
669
+ cleanup_swap_file "${swap_file_path}"
670
+ return 1
671
+ fi
672
+
673
+ # Add to fstab if not already present (using awk for precise matching)
674
+ if ! awk -v path="${swap_file_path}" '$1 == path && $3 == "swap" {found=1} END {exit !found}' /etc/fstab 2>/dev/null; then
675
+ if ! echo "${swap_file_path} none swap sw,pri=1 0 0" | sudo tee -a /etc/fstab >/dev/null; then
676
+ echo "⚠️ Warning: Failed to add swap to fstab (swap is still active)"
677
+ fi
678
+ fi
679
+
680
+ # Final verification
681
+ if check_swap_exists "${swap_file_path}"; then
682
+ echo "✅ Swap file setup complete ($(numfmt --to=iec ${size_bytes}))"
683
+ return 0
684
+ else
685
+ echo "❌ Failed to verify swap file setup"
686
+ return 1
687
+ fi
688
+ else
689
+ echo "✅ Skipping swap file creation (L4T installed on SD card)"
690
+ return 0
691
+ fi
692
+ }
693
+
694
+ # ================================================================================
695
+ # zRAM
696
+ # ================================================================================
697
+
698
+ check_zram_state() {
699
+ if swapon --noheadings --raw --show=NAME | grep -q 'zram'; then
700
+ echo "active"
701
+ elif file_exists /etc/modules-load.d/zram.conf; then
702
+ echo "configured"
703
+ else
704
+ echo "disabled"
705
+ fi
706
+ }
707
+
708
+ # Disable zRAM (the Thor setup uses a file-backed swap instead)
709
+ setup_zram() {
710
+ local zram_enabled=${1:-"no"}
711
+
712
+ # If zRAM should be enabled
713
+ if is_true "${zram_enabled}"; then
714
+ echo "❌ zRAM is not enabled by the mjolnir Thor setup (file-backed swap instead)."
715
+ return 1
716
+ fi
717
+
718
+ local zram_state
719
+ zram_state=$(check_zram_state)
720
+ # Disable zRAM if it's enabled
721
+ if [ "${zram_state}" != "disabled" ]; then
722
+ # Stop and disable the zram service
723
+ systemctl_disable_service zram
724
+ systemctl_disable_service nvzramconfig
725
+
726
+ local d
727
+ for d in /dev/zram*; do
728
+ [[ -e "$d" ]] || continue
729
+ sudo swapoff "$d" 2>/dev/null || true
730
+ echo 1 | sudo tee /sys/block/"$(basename "$d")"/reset >/dev/null || true
731
+ echo "✅ Disabled $d..."
732
+ done
733
+
734
+ # Unload the zram module
735
+ sudo rmmod zram 2>/dev/null || true
736
+
737
+ sudo rm -f /etc/modules-load.d/zram.conf /etc/modprobe.d/zram.conf \
738
+ /etc/udev/rules.d/99-zram.rules /etc/systemd/system/zram.service
739
+ sudo systemctl daemon-reload
740
+
741
+ echo "✅ zRAM has been disabled"
742
+ else
743
+ echo "✅ zRAM is already disabled"
744
+ fi
745
+ }
746
+
747
+ # Configure memory settings
748
+ configure_jetson_memory() {
749
+ # Setup swap file
750
+ setup_swap_file
751
+
752
+ # Configure zRAM (the Thor setup keeps it off)
753
+ setup_zram "false"
754
+
755
+ # Display current memory configuration
756
+ echo -e "\n=== Current Memory Configuration ==="
757
+ free -h
758
+
759
+ # Display swap configuration if supported
760
+ if ! is_l4t_installed_on_sdcard; then
761
+ echo -e "\nSwap configuration:"
762
+ swapon -s
763
+ fi
764
+
765
+ echo
766
+ echo "✅ Memory setup complete"
767
+ }
768
+
769
+ # ================================================================================
770
+ # GUI
771
+ # ================================================================================
772
+
773
+ # Function to get current GUI state
774
+ get_gui_state() {
775
+ if systemctl get-default | grep -q "graphical.target"; then
776
+ echo "enabled"
777
+ else
778
+ echo "disabled"
779
+ fi
780
+ }
781
+
782
+ # Function to enable GUI
783
+ enable_gui() {
784
+ sudo systemctl set-default graphical.target
785
+
786
+ if is_true "$(get_gui_state)"; then
787
+ echo "✅ GUI has been enabled on boot"
788
+ return 0
789
+ else
790
+ echo "❌ Failed to enable GUI"
791
+ return 1
792
+ fi
793
+ }
794
+
795
+ # Function to disable GUI
796
+ disable_gui() {
797
+ sudo systemctl set-default multi-user.target
798
+
799
+ if ! is_true "$(get_gui_state)"; then
800
+ echo "✅ GUI has been disabled on boot"
801
+ return 0
802
+ else
803
+ echo "❌ Failed to disable GUI"
804
+ return 1
805
+ fi
806
+ }
807
+
808
+ configure_gui() {
809
+ local gui_enabled=${1-"no"}
810
+ local current_state
811
+ current_state=$(get_gui_state)
812
+
813
+ if is_true "$gui_enabled"; then
814
+ if [ "$current_state" = "disabled" ]; then
815
+ enable_gui
816
+ else
817
+ echo "✅ GUI is already enabled"
818
+ fi
819
+ else
820
+ if [ "$current_state" = "enabled" ]; then
821
+ disable_gui
822
+ else
823
+ echo "✅ GUI is already disabled"
824
+ fi
825
+ fi
826
+ }
827
+
828
+ # ================================================================================
829
+ # FAN (merged from scripts/hw/fan-profiles.sh — one process with the rest)
830
+ #
831
+ # 1. BACKUP (first run only): untouched $CONF is dumped to $CONF.bck.
832
+ # The .bck ALWAYS holds the original — the clean rollback point.
833
+ # 2. INJECT (per-profile gated): the two performance fan profiles
834
+ # (tuned on the AGX Thor) are inserted before the THERMAL_GROUP
835
+ # section — each ONLY if its block is not already present:
836
+ # FAN_PROFILE recommended — balanced warmth/acoustics
837
+ # FAN_PROFILE max — sustained full-load cooling (bench)
838
+ # 3. FAN MODE SELECTION: FAN_DEFAULT_PROFILE <--fan-profile> (default
839
+ # "recommended", not the stock "cool").
840
+ # 4. Restart nvfancontrol and print the daemon's active state.
841
+ # ================================================================================
842
+
843
+ FAN_CONF="${MJOLNIR_NVFANCONF:-/etc/nvfancontrol.conf}"
844
+
845
+ write_fan_profile_blocks() {
846
+ local dir="$1"
847
+ cat > "$dir/profile_recommended.conf" <<'EOF'
848
+ FAN_PROFILE recommended {
849
+ #TEMP HYST PWM RPM
850
+ 0 0 255 5371
851
+ 10 0 220 4700
852
+ 20 0 180 3900
853
+ 30 0 140 3000
854
+ 40 0 102 2400
855
+ 55 0 90 2100
856
+ 70 0 80 1800
857
+ 115 0 80 1800
858
+ }
859
+ EOF
860
+
861
+ cat > "$dir/profile_max.conf" <<'EOF'
862
+ FAN_PROFILE max {
863
+ #TEMP HYST PWM RPM
864
+ 0 0 255 5371
865
+ 15 0 240 5000
866
+ 25 0 220 4700
867
+ 35 0 195 4200
868
+ 50 0 170 3700
869
+ 70 0 120 2700
870
+ 115 0 120 2700
871
+ }
872
+ EOF
873
+ }
874
+
875
+ # $1=src $2=dst — inserts the missing profile block(s) before the
876
+ # THERMAL_GROUP anchor and selects the fan mode (FAN_DEFAULT_PROFILE).
877
+ fan_transform() {
878
+ awk -v do_rec="$DO_REC" -v do_max="$DO_MAX" -v sel="$FAN_PROFILE" \
879
+ -v src="$1" \
880
+ -v f_rec="$FAN_TMP/profile_recommended.conf" \
881
+ -v f_max="$FAN_TMP/profile_max.conf" '
882
+ /^[[:space:]]*FAN_DEFAULT_PROFILE/ { sub(/FAN_DEFAULT_PROFILE.*/, "FAN_DEFAULT_PROFILE " sel) }
883
+ !done && $0 ~ /^[[:space:]]*THERMAL_GROUP[[:space:]]0/ {
884
+ done=1
885
+ if (do_rec) while ((getline l < f_rec) > 0) print l
886
+ if (do_max) while ((getline l < f_max) > 0) print l
887
+ }
888
+ { print }
889
+ END {
890
+ if (!done) {
891
+ print "setup-thor: no THERMAL_GROUP anchor in " src " — refusing to guess" > "/dev/stderr"
892
+ exit 3
893
+ }
894
+ }
895
+ ' "$1" > "$2"
896
+ }
897
+
898
+ do_fan() {
899
+ local conf="$FAN_CONF"
900
+ local bck="$conf.bck"
901
+
902
+ if ! file_exists "$conf"; then
903
+ echo "ℹ️ No nvfancontrol conf at $conf — skipping fan."
904
+ return 0
905
+ fi
906
+
907
+ if ! is_command_available nvfancontrol; then
908
+ echo "ℹ️ nvfancontrol not available — skipping fan."
909
+ return 0
910
+ fi
911
+
912
+ local have_rec have_max cur_default
913
+ have_rec=$(grep -cE "FAN_PROFILE recommended[[:space:]]*\{" "$conf" || true)
914
+ have_max=$(grep -cE "FAN_PROFILE max[[:space:]]*\{" "$conf" || true)
915
+ cur_default="$(awk '/^[[:space:]]*FAN_DEFAULT_PROFILE/ {print $2; exit}' "$conf")"
916
+ cur_default="${cur_default:-<unset>}"
917
+
918
+ local do_rec=0 do_max=0
919
+ [ "$have_rec" -eq 0 ] && do_rec=1
920
+ [ "$have_max" -eq 0 ] && do_max=1
921
+ DO_REC=$do_rec
922
+ DO_MAX=$do_max
923
+
924
+ # Stock profiles ship with the L4T conf; recommended/max are installed here.
925
+ case "$FAN_PROFILE" in
926
+ cool|quiet)
927
+ if ! grep -qE "FAN_PROFILE ${FAN_PROFILE}[[:space:]]*\{" "$conf"; then
928
+ echo "❌ fan profile '$FAN_PROFILE' not in $conf" >&2
929
+ return 1
930
+ fi
931
+ ;;
932
+ esac
933
+
934
+ local pending=0
935
+ [ -e "$bck" ] || pending=1
936
+ [ "$do_rec" = 1 ] && pending=1
937
+ [ "$do_max" = 1 ] && pending=1
938
+ [ "$cur_default" != "$FAN_PROFILE" ] && pending=1
939
+
940
+ echo "fan profiles: $conf"
941
+ if [ -e "$bck" ]; then
942
+ echo " backup: $bck (exists — original preserved)"
943
+ else
944
+ echo " backup: $conf -> $bck (first run — will be created)"
945
+ fi
946
+ if [ "$do_rec" = 1 ]; then
947
+ echo " inject: FAN_PROFILE recommended (not present — will be added)"
948
+ else
949
+ echo " inject: FAN_PROFILE recommended (already present — skipped)"
950
+ fi
951
+ if [ "$do_max" = 1 ]; then
952
+ echo " inject: FAN_PROFILE max (not present — will be added)"
953
+ else
954
+ echo " inject: FAN_PROFILE max (already present — skipped)"
955
+ fi
956
+ if [ "$cur_default" != "$FAN_PROFILE" ]; then
957
+ echo " fan mode: FAN_DEFAULT_PROFILE $cur_default -> $FAN_PROFILE"
958
+ else
959
+ echo " fan mode: FAN_DEFAULT_PROFILE $FAN_PROFILE (already selected)"
960
+ fi
961
+
962
+ if [ "$pending" = 0 ]; then
963
+ echo "✅ Fan already configured (profile: $FAN_PROFILE)."
964
+ return 0
965
+ fi
966
+
967
+ FAN_TMP="$(mktemp -d)"
968
+ trap 'rm -rf "$FAN_TMP"' EXIT
969
+ write_fan_profile_blocks "$FAN_TMP"
970
+
971
+ # First run only: the .bck always holds the untouched original.
972
+ [ -e "$bck" ] || sudo cp -a -- "$conf" "$bck"
973
+ fan_transform "$conf" "$FAN_TMP/new.conf"
974
+ sudo cp -- "$FAN_TMP/new.conf" "$conf"
975
+
976
+ # Remove the old status so the daemon reloads fresh, then restart.
977
+ sudo rm -f /var/lib/nvfancontrol/status
978
+ systemctl_restart_service nvfancontrol
979
+
980
+ echo "✅ Fan profile installed; default: $FAN_PROFILE."
981
+ echo " active state:"
982
+ sudo nvfancontrol -q 2>&1 | grep -iE 'profile|governor' | sed 's/^/ /' || true
983
+ }
984
+
985
+ # ================================================================================
986
+ # HOST / SYSTEM CONFIGURATION
987
+ # ================================================================================
988
+
989
+ host_optimizations() {
990
+ systemctl_disable_service nvargus-daemon.service
991
+ systemctl_disable_service cups
992
+ systemctl_disable_service ModemManager
993
+ }
994
+
995
+ check_jetson_clocks() {
996
+ local state
997
+ state=$(sudo jetson_clocks --show | grep -oP '(?<=FreqOverride=)\d+')
998
+
999
+ is_true "$state"
1000
+ }
1001
+
1002
+ start_jetson_clocks_service() {
1003
+ local service_name="jetson_clocks.service"
1004
+ local service_path="/etc/systemd/system/${service_name}"
1005
+
1006
+ # 1) Ensure the unit file exists
1007
+ if ! systemctl_service_exists "$service_name"; then
1008
+ echo "ℹ️ $service_name not found; creating at $service_path"
1009
+ sudo tee "$service_path" >/dev/null <<'EOF'
1010
+ [Unit]
1011
+ Description=Jetson Clocks Service
1012
+ After=multi-user.target
1013
+
1014
+ [Service]
1015
+ Type=oneshot
1016
+ ExecStart=/usr/bin/jetson_clocks
1017
+ RemainAfterExit=true
1018
+
1019
+ [Install]
1020
+ WantedBy=multi-user.target
1021
+ EOF
1022
+
1023
+ echo "ℹ️ Reloading systemd daemon"
1024
+ sudo systemctl daemon-reload
1025
+
1026
+ echo "ℹ️ Enabling $service_name to start on boot"
1027
+ systemctl_enable_service "$service_name"
1028
+ fi
1029
+
1030
+ # 2) Check if the service is active
1031
+ if ! systemctl_service_is_active "$service_name"; then
1032
+ echo "ℹ️ Starting $service_name"
1033
+ systemctl_start_service "$service_name"
1034
+ echo "✅ $service_name started."
1035
+ echo "ℹ️ Current clock settings:"
1036
+ sudo jetson_clocks --show
1037
+ fi
1038
+
1039
+ # 3) Final health check
1040
+ if check_jetson_clocks; then
1041
+ echo "✅ Jetson Clocks is enabled and running."
1042
+ else
1043
+ echo "❌ Failed to enable Jetson Clocks lock."
1044
+ return 1
1045
+ fi
1046
+ }
1047
+
1048
+ # ================================================================================
1049
+ # POWER MODE
1050
+ # ================================================================================
1051
+
1052
+ check_nvpmodel_installed() {
1053
+ if ! is_command_available nvpmodel; then
1054
+ echo "❌ Error: Missing required dependency: nvpmodel"
1055
+ echo " Please check your Jetson system installation"
1056
+ exit 1
1057
+ fi
1058
+ }
1059
+
1060
+ get_power_mode() {
1061
+ check_nvpmodel_installed
1062
+
1063
+ nvpmodel -q | grep "NV Power Mode" | cut -d':' -f2 | xargs
1064
+ }
1065
+
1066
+ get_power_modes() {
1067
+ local file="${1:-/etc/nvpmodel.conf}"
1068
+ # declare global associative array
1069
+ declare -gA JETSON_POWER_MODES=()
1070
+
1071
+ local line id name
1072
+ while IFS= read -r line; do
1073
+ if [[ $line =~ \<[[:space:]]*POWER_MODEL[[:space:]]+ID=([0-9]+)[[:space:]]+NAME=([^[:space:]>]+) ]]; then
1074
+ id="${BASH_REMATCH[1]}"
1075
+ name="${BASH_REMATCH[2]}"
1076
+ JETSON_POWER_MODES[$id]="$name"
1077
+ fi
1078
+ done < "$file"
1079
+ }
1080
+
1081
+ get_power_mode_index_by_name() {
1082
+ local substr="$1"
1083
+ local id
1084
+
1085
+ for id in "${!JETSON_POWER_MODES[@]}"; do
1086
+ if [[ "${JETSON_POWER_MODES[$id]}" == *"$substr"* ]]; then
1087
+ printf '%s\n' "$id"
1088
+ return 0
1089
+ fi
1090
+ done
1091
+ return 1
1092
+ }
1093
+
1094
+ get_power_mode_config_file() {
1095
+ check_nvpmodel_installed
1096
+
1097
+ nvpmodel -q --verbose 2>&1 | grep -m1 'Config file:' | awk -F': ' '{print $NF}'
1098
+ }
1099
+
1100
+ # Configure power mode
1101
+ setup_power_mode() {
1102
+ local current_mode
1103
+ local power_config_path
1104
+ current_mode="$(get_power_mode)"
1105
+ power_config_path="$(get_power_mode_config_file)"
1106
+
1107
+ get_power_modes
1108
+
1109
+ # First try "SUPER", then "MAXN"
1110
+ if MAXN_POWER_MODE_ID=$(get_power_mode_index_by_name SUPER); then
1111
+ :
1112
+ elif MAXN_POWER_MODE_ID=$(get_power_mode_index_by_name MAXN); then
1113
+ :
1114
+ else
1115
+ echo "❌ No matching MAXN or MAXN_SUPER power mode!"
1116
+ exit 1
1117
+ fi
1118
+
1119
+ if [ "$current_mode" = "${JETSON_POWER_MODES[$MAXN_POWER_MODE_ID]}" ]; then
1120
+ echo "✅ Power mode already set to $current_mode, skipping..."
1121
+ return 0
1122
+ fi
1123
+
1124
+ echo "ℹ️ Setting power mode to mode ${JETSON_POWER_MODES[$MAXN_POWER_MODE_ID]} (this will be applied after reboot)..."
1125
+
1126
+ # Use -f flag to suppress the interactive reboot prompt
1127
+ if sudo nvpmodel -m "$MAXN_POWER_MODE_ID" -f "$power_config_path" > /dev/null; then
1128
+ echo "✅ Power mode change scheduled. A reboot will be required to apply this change."
1129
+ REBOOT_REQUIRED=1
1130
+ return 0
1131
+ else
1132
+ echo "❌ Failed to set power mode"
1133
+ return 1
1134
+ fi
1135
+ }
1136
+
1137
+ # ================================================================================
1138
+ # MAIN
1139
+ # ================================================================================
1140
+
1141
+ print_plan() {
1142
+ echo
1143
+ echo "Plan (dry-run — nothing will change):"
1144
+
1145
+ local i=0
1146
+ _plan() {
1147
+ local name="$1" label="$2"
1148
+ i=$((i + 1))
1149
+ if is_skipped "$name"; then
1150
+ printf ' %2d · %-8s %s\n' "$i" "$name" "$label (skipped)"
1151
+ else
1152
+ printf ' %2d → %-8s %s\n' "$i" "$name" "$label"
1153
+ fi
1154
+ }
1155
+ _plan gui "$( [ "$KEEP_GUI" = "1" ] && echo 'keep GUI (boot target unchanged)' || echo 'disable GUI on boot (multi-user.target)')"
1156
+ _plan upgrade "$( [ "$UPGRADE" = "1" ] && echo 'apt update + full-upgrade (+ nvidia-jetpack, curl, wget)' || echo 'skip the system upgrade')"
1157
+ _plan pip "pip + setuptools + wheel (get-pip.py fallback)"
1158
+ _plan docker "docker-ce + nvidia-container-toolkit, user in docker group, nvidia default runtime, service up + enabled"
1159
+ _plan jtop "jetson-stats (jtop)"
1160
+ _plan memory "${SWAP_SIZE} GB swap file at /mnt (zRAM off)"
1161
+ _plan fan "install recommended+max fan profiles (gated, .bck backup), FAN_DEFAULT_PROFILE: ${FAN_PROFILE}"
1162
+ _plan host "disable nvargus-daemon / cups / ModemManager"
1163
+ _plan clocks "jetson_clocks.service (lock max CPU/GPU/EMC clocks)"
1164
+ _plan power "nvpmodel → MAXN/SUPER (reboot to apply)"
1165
+ echo
1166
+ }
1167
+
1168
+ # Bail early if not running on a Jetson (no device-tree)
1169
+ if [ ! -r /proc/device-tree/compatible ]; then
1170
+ echo "❌ /proc/device-tree/compatible not found; not a Jetson platform."
1171
+ exit 1
1172
+ fi
1173
+
1174
+ # Prepare a "table" with spaces or tabs
1175
+ cat <<EOF | column -t
1176
+ ✅ User ${USER}
1177
+ ✅ SoM ${SOC_TYPE}
1178
+ ✅ Memory ${SOC_MEMORY}GB
1179
+ ✅ DevKit ${IS_NVIDIA_CARRIER}
1180
+ EOF
1181
+
1182
+ echo
1183
+ if is_command_available nvidia-smi; then
1184
+ nvidia-smi
1185
+ fi
1186
+
1187
+ if [ "$DRY_RUN" = "1" ]; then
1188
+ print_plan
1189
+ exit 0
1190
+ fi
1191
+
1192
+ if [ "$ASSUME_YES" != "1" ]; then
1193
+ ask_should_proceed
1194
+ fi
1195
+
1196
+ # 1. Boot target: headless server (multi-user), GUI off by default
1197
+ if is_skipped gui; then
1198
+ echo "ℹ️ Skipping: gui"
1199
+ elif [ "$KEEP_GUI" = "1" ]; then
1200
+ echo "ℹ️ Keeping the GUI boot target (--keep-gui)."
1201
+ else
1202
+ configure_gui "no"
1203
+ fi
1204
+
1205
+ # 2. System upgrade (apt), JetPack + curl + wget
1206
+ if is_skipped upgrade; then
1207
+ echo "ℹ️ Skipping: upgrade"
1208
+ else
1209
+ upgrade_system
1210
+ fi
1211
+
1212
+ # 3. Python tooling
1213
+ if is_skipped pip; then
1214
+ echo "ℹ️ Skipping: pip"
1215
+ else
1216
+ install_pip
1217
+ fi
1218
+
1219
+ # 4. Docker + NVIDIA runtime
1220
+ if is_skipped docker; then
1221
+ echo "ℹ️ Skipping: docker"
1222
+ else
1223
+ install_docker
1224
+ setup_docker_group
1225
+ configure_docker_runtime
1226
+ ensure_docker_running_and_enabled
1227
+ fi
1228
+
1229
+ # 5. Monitor
1230
+ if is_skipped jtop; then
1231
+ echo "ℹ️ Skipping: jtop"
1232
+ else
1233
+ install_jtop
1234
+ fi
1235
+
1236
+ # 6. Memory: swap file + zRAM off
1237
+ if is_skipped memory; then
1238
+ echo "ℹ️ Skipping: memory"
1239
+ else
1240
+ configure_jetson_memory
1241
+ fi
1242
+
1243
+ # 7. Fan profile
1244
+ if is_skipped fan; then
1245
+ echo "ℹ️ Skipping: fan"
1246
+ else
1247
+ do_fan
1248
+ fi
1249
+
1250
+ # 8. Host service cleanup
1251
+ if is_skipped host; then
1252
+ echo "ℹ️ Skipping: host"
1253
+ else
1254
+ host_optimizations
1255
+ fi
1256
+
1257
+ # 9. Max clocks (jetson_clocks)
1258
+ if is_skipped clocks; then
1259
+ echo "ℹ️ Skipping: clocks"
1260
+ else
1261
+ start_jetson_clocks_service
1262
+ fi
1263
+
1264
+ # 10. Power mode (MAXN/SUPER)
1265
+ if is_skipped power; then
1266
+ echo "ℹ️ Skipping: power"
1267
+ else
1268
+ setup_power_mode
1269
+ fi
1270
+
1271
+ echo "✅ Jetson Thor configuration completed."
1272
+
1273
+ if [ "$REBOOT_REQUIRED" = "1" ]; then
1274
+ if [ "$DO_REBOOT" = "1" ]; then
1275
+ echo "Rebooting, please wait..."
1276
+ sudo reboot
1277
+ elif [ -t 0 ]; then
1278
+ if ask_yes_no "A reboot is required to apply the power-mode change — reboot now?"; then
1279
+ echo "Rebooting, please wait..."
1280
+ sudo reboot
1281
+ else
1282
+ echo "ℹ️ Reboot whenever convenient (the power mode is already scheduled)."
1283
+ fi
1284
+ else
1285
+ echo "ℹ️ Reboot required to apply the power-mode change (rerun with --reboot, or reboot manually)."
1286
+ fi
1287
+ fi