hte-cli 0.2.27__py3-none-any.whl → 0.2.29__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.
- hte_cli/cli.py +284 -8
- hte_cli/image_utils.py +187 -0
- {hte_cli-0.2.27.dist-info → hte_cli-0.2.29.dist-info}/METADATA +1 -1
- {hte_cli-0.2.27.dist-info → hte_cli-0.2.29.dist-info}/RECORD +6 -6
- {hte_cli-0.2.27.dist-info → hte_cli-0.2.29.dist-info}/WHEEL +0 -0
- {hte_cli-0.2.27.dist-info → hte_cli-0.2.29.dist-info}/entry_points.txt +0 -0
hte_cli/cli.py
CHANGED
|
@@ -245,6 +245,10 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
245
245
|
extract_images_from_compose,
|
|
246
246
|
extract_image_platforms_from_compose,
|
|
247
247
|
pull_image_with_progress,
|
|
248
|
+
check_image_architecture_matches_host,
|
|
249
|
+
fix_image_architecture,
|
|
250
|
+
get_host_docker_platform,
|
|
251
|
+
is_running_in_linux_vm_on_arm,
|
|
248
252
|
)
|
|
249
253
|
|
|
250
254
|
# Create event streamer
|
|
@@ -324,9 +328,26 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
324
328
|
if images:
|
|
325
329
|
from hte_cli.image_utils import check_image_exists_locally
|
|
326
330
|
|
|
331
|
+
# Detect host architecture for smart image handling
|
|
332
|
+
is_linux_arm = is_running_in_linux_vm_on_arm()
|
|
333
|
+
host_platform = get_host_docker_platform()
|
|
334
|
+
|
|
335
|
+
if is_linux_arm:
|
|
336
|
+
console.print(
|
|
337
|
+
f"[yellow]![/yellow] Detected [bold]Linux ARM64[/bold] environment"
|
|
338
|
+
)
|
|
339
|
+
console.print(
|
|
340
|
+
f" [dim]Will verify cached images match host architecture ({host_platform})[/dim]"
|
|
341
|
+
)
|
|
342
|
+
console.print(
|
|
343
|
+
f" [dim]Mismatched images will be automatically re-pulled[/dim]"
|
|
344
|
+
)
|
|
345
|
+
console.print()
|
|
346
|
+
|
|
327
347
|
console.print(f"[bold]Step 2:[/bold] Pulling {len(images)} Docker image(s)...")
|
|
328
348
|
pull_start = time.monotonic()
|
|
329
349
|
pull_errors = {}
|
|
350
|
+
x86_images_on_arm = [] # Track x86 images that need QEMU
|
|
330
351
|
|
|
331
352
|
for img in images:
|
|
332
353
|
short_name = img.split("/")[-1][:40]
|
|
@@ -334,13 +355,71 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
334
355
|
|
|
335
356
|
# Check if already cached
|
|
336
357
|
if check_image_exists_locally(img):
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
358
|
+
# Verify architecture matches host (important for Linux ARM64)
|
|
359
|
+
matches, image_arch, host_arch = check_image_architecture_matches_host(img)
|
|
360
|
+
|
|
361
|
+
if matches:
|
|
362
|
+
# Show architecture info on Linux ARM64 for transparency
|
|
363
|
+
if is_linux_arm and image_arch:
|
|
364
|
+
console.print(
|
|
365
|
+
f" [green]✓[/green] {short_name} [dim](cached, arch: {image_arch})[/dim]"
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
console.print(f" [green]✓[/green] {short_name} [dim](cached)[/dim]")
|
|
369
|
+
cached_images.append(img)
|
|
370
|
+
continue
|
|
371
|
+
else:
|
|
372
|
+
# Architecture mismatch detected - this is the key fix for Linux ARM64
|
|
373
|
+
console.print(
|
|
374
|
+
f" [yellow]⚠[/yellow] {short_name} [yellow]architecture mismatch![/yellow]"
|
|
375
|
+
)
|
|
376
|
+
console.print(
|
|
377
|
+
f" [dim]Cached image: {image_arch} | Host: {host_arch}[/dim]"
|
|
378
|
+
)
|
|
379
|
+
console.print(
|
|
380
|
+
f" [dim]Removing cached image and re-pulling correct architecture...[/dim]"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
needed_fix, fix_msg = fix_image_architecture(img)
|
|
384
|
+
if needed_fix:
|
|
385
|
+
console.print(
|
|
386
|
+
f" [green]✓[/green] {short_name} [green]fixed![/green] [dim]({fix_msg})[/dim]"
|
|
387
|
+
)
|
|
388
|
+
pulled_images.append(img)
|
|
389
|
+
continue
|
|
390
|
+
elif "failed to re-pull" in fix_msg:
|
|
391
|
+
# No ARM variant available - this is an x86-only image
|
|
392
|
+
# Re-pull the amd64 version and warn about QEMU
|
|
393
|
+
console.print(
|
|
394
|
+
f" [dim]No ARM variant available - re-pulling x86 version...[/dim]"
|
|
395
|
+
)
|
|
396
|
+
success = pull_image_with_progress(img)
|
|
397
|
+
if success:
|
|
398
|
+
console.print(
|
|
399
|
+
f" [yellow]![/yellow] {short_name} [dim](x86-only image, needs QEMU)[/dim]"
|
|
400
|
+
)
|
|
401
|
+
x86_images_on_arm.append(img)
|
|
402
|
+
pulled_images.append(img)
|
|
403
|
+
continue
|
|
404
|
+
else:
|
|
405
|
+
console.print(f" [red]✗[/red] {short_name} [dim](failed to pull)[/dim]")
|
|
406
|
+
failed_images.append(img)
|
|
407
|
+
pull_errors[img] = "failed to pull x86 fallback"
|
|
408
|
+
continue
|
|
409
|
+
else:
|
|
410
|
+
console.print(f" [red]✗[/red] {short_name} [dim]({fix_msg})[/dim]")
|
|
411
|
+
failed_images.append(img)
|
|
412
|
+
pull_errors[img] = fix_msg
|
|
413
|
+
continue
|
|
340
414
|
|
|
341
415
|
# Need to pull - show progress
|
|
342
416
|
last_status = ["connecting..."]
|
|
343
417
|
last_error = [""]
|
|
418
|
+
|
|
419
|
+
# Use platform from compose if specified, otherwise let Docker decide
|
|
420
|
+
# (Docker will prefer native arch for multi-arch images, or pull what's available)
|
|
421
|
+
pull_platform = platform
|
|
422
|
+
|
|
344
423
|
with console.status(
|
|
345
424
|
f"[yellow]↓[/yellow] {short_name} [dim]connecting...[/dim]"
|
|
346
425
|
) as status:
|
|
@@ -364,14 +443,37 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
364
443
|
last_error[0] = line
|
|
365
444
|
|
|
366
445
|
success = pull_image_with_progress(
|
|
367
|
-
img, platform=
|
|
446
|
+
img, platform=pull_platform, on_progress=show_progress
|
|
368
447
|
)
|
|
369
448
|
|
|
370
449
|
if success:
|
|
371
|
-
|
|
450
|
+
# On Linux ARM64, verify pulled image architecture
|
|
451
|
+
if is_linux_arm:
|
|
452
|
+
from hte_cli.image_utils import get_image_architecture
|
|
453
|
+
pulled_arch = get_image_architecture(img)
|
|
454
|
+
|
|
455
|
+
if pulled_arch == "arm64":
|
|
456
|
+
console.print(
|
|
457
|
+
f" [green]✓[/green] {short_name} [dim](downloaded, arch: arm64)[/dim]"
|
|
458
|
+
)
|
|
459
|
+
elif pulled_arch == "amd64":
|
|
460
|
+
# x86 image on ARM host - needs QEMU emulation
|
|
461
|
+
console.print(
|
|
462
|
+
f" [yellow]![/yellow] {short_name} [dim](downloaded, arch: amd64)[/dim]"
|
|
463
|
+
)
|
|
464
|
+
console.print(
|
|
465
|
+
f" [yellow]This is an x86 image - requires QEMU emulation on ARM[/yellow]"
|
|
466
|
+
)
|
|
467
|
+
x86_images_on_arm.append(img)
|
|
468
|
+
else:
|
|
469
|
+
console.print(
|
|
470
|
+
f" [green]✓[/green] {short_name} [dim](downloaded)[/dim]"
|
|
471
|
+
)
|
|
472
|
+
else:
|
|
473
|
+
console.print(f" [green]✓[/green] {short_name} [dim](downloaded)[/dim]")
|
|
372
474
|
pulled_images.append(img)
|
|
373
475
|
else:
|
|
374
|
-
platform_note = f" (platform: {
|
|
476
|
+
platform_note = f" (platform: {pull_platform})" if pull_platform else ""
|
|
375
477
|
console.print(f" [red]✗[/red] {short_name}{platform_note} [dim](failed)[/dim]")
|
|
376
478
|
if last_error[0]:
|
|
377
479
|
console.print(f" [dim]{last_error[0][:60]}[/dim]")
|
|
@@ -387,6 +489,19 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
387
489
|
)
|
|
388
490
|
console.print()
|
|
389
491
|
|
|
492
|
+
# Warn about x86 images on ARM that need QEMU
|
|
493
|
+
if x86_images_on_arm:
|
|
494
|
+
console.print(
|
|
495
|
+
f"[yellow]⚠ Warning:[/yellow] {len(x86_images_on_arm)} x86 image(s) detected on ARM host"
|
|
496
|
+
)
|
|
497
|
+
console.print(
|
|
498
|
+
" These require QEMU emulation. If container fails to start, run:"
|
|
499
|
+
)
|
|
500
|
+
console.print(
|
|
501
|
+
" [bold]docker run --privileged --rm tonistiigi/binfmt --install all[/bold]"
|
|
502
|
+
)
|
|
503
|
+
console.print()
|
|
504
|
+
|
|
390
505
|
# Fail fast if any required image couldn't be pulled
|
|
391
506
|
if failed_images:
|
|
392
507
|
console.print(
|
|
@@ -395,8 +510,15 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
395
510
|
console.print()
|
|
396
511
|
console.print("[yellow]Troubleshooting:[/yellow]")
|
|
397
512
|
console.print(" 1. Check Docker is running: docker info")
|
|
398
|
-
|
|
399
|
-
|
|
513
|
+
|
|
514
|
+
# Architecture-specific advice
|
|
515
|
+
if is_linux_arm:
|
|
516
|
+
console.print(f" 2. You're on Linux ARM64 - try: docker pull <image> --platform linux/arm64")
|
|
517
|
+
console.print(" 3. For x86-only images, enable QEMU: docker run --privileged --rm tonistiigi/binfmt --install all")
|
|
518
|
+
else:
|
|
519
|
+
console.print(" 2. Try manual pull: docker pull <image>")
|
|
520
|
+
|
|
521
|
+
console.print(" 4. Check network connectivity")
|
|
400
522
|
console.print()
|
|
401
523
|
console.print("Session remains active - you can retry with: hte-cli session join " + session_id)
|
|
402
524
|
sys.exit(1)
|
|
@@ -672,6 +794,160 @@ def tasks_pull_images(ctx, count: int):
|
|
|
672
794
|
console.print("[yellow]Image pulling not yet implemented.[/yellow]")
|
|
673
795
|
|
|
674
796
|
|
|
797
|
+
@cli.command("diagnose")
|
|
798
|
+
def diagnose_cmd():
|
|
799
|
+
"""
|
|
800
|
+
Diagnose Docker and architecture setup.
|
|
801
|
+
|
|
802
|
+
Checks Docker installation, architecture detection, and image compatibility.
|
|
803
|
+
Useful for troubleshooting before running tasks.
|
|
804
|
+
"""
|
|
805
|
+
import subprocess
|
|
806
|
+
import sys as system_module
|
|
807
|
+
from hte_cli.image_utils import (
|
|
808
|
+
get_host_architecture,
|
|
809
|
+
get_host_docker_platform,
|
|
810
|
+
is_running_in_linux_vm_on_arm,
|
|
811
|
+
get_image_architecture,
|
|
812
|
+
check_image_exists_locally,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
console.print("[bold]HTE-CLI Diagnostics[/bold]")
|
|
816
|
+
console.print("=" * 50)
|
|
817
|
+
console.print()
|
|
818
|
+
|
|
819
|
+
# CLI version
|
|
820
|
+
console.print(f"[bold]CLI Version:[/bold] {__version__}")
|
|
821
|
+
console.print()
|
|
822
|
+
|
|
823
|
+
# Platform info
|
|
824
|
+
console.print("[bold]Platform:[/bold]")
|
|
825
|
+
host_arch = get_host_architecture()
|
|
826
|
+
host_platform = get_host_docker_platform()
|
|
827
|
+
is_linux_arm = is_running_in_linux_vm_on_arm()
|
|
828
|
+
|
|
829
|
+
console.print(f" OS: {system_module.platform}")
|
|
830
|
+
console.print(f" Architecture: {host_arch}")
|
|
831
|
+
console.print(f" Docker platform: {host_platform or 'unknown'}")
|
|
832
|
+
|
|
833
|
+
if is_linux_arm:
|
|
834
|
+
console.print()
|
|
835
|
+
console.print("[yellow]⚠ Linux ARM64 detected![/yellow]")
|
|
836
|
+
console.print(" This environment may have architecture compatibility issues.")
|
|
837
|
+
console.print(" The CLI will automatically handle multi-arch images.")
|
|
838
|
+
console.print(" For x86-only images (CTF challenges), QEMU emulation is required.")
|
|
839
|
+
console.print()
|
|
840
|
+
|
|
841
|
+
# Docker checks
|
|
842
|
+
console.print("[bold]Docker:[/bold]")
|
|
843
|
+
|
|
844
|
+
# Check Docker installed
|
|
845
|
+
try:
|
|
846
|
+
result = subprocess.run(
|
|
847
|
+
["docker", "--version"],
|
|
848
|
+
capture_output=True,
|
|
849
|
+
text=True,
|
|
850
|
+
timeout=10,
|
|
851
|
+
)
|
|
852
|
+
if result.returncode == 0:
|
|
853
|
+
console.print(f" [green]✓[/green] Docker: {result.stdout.strip()}")
|
|
854
|
+
else:
|
|
855
|
+
console.print(" [red]✗[/red] Docker not working")
|
|
856
|
+
except Exception as e:
|
|
857
|
+
console.print(f" [red]✗[/red] Docker not found: {e}")
|
|
858
|
+
|
|
859
|
+
# Check Docker daemon
|
|
860
|
+
try:
|
|
861
|
+
result = subprocess.run(
|
|
862
|
+
["docker", "info"],
|
|
863
|
+
capture_output=True,
|
|
864
|
+
text=True,
|
|
865
|
+
timeout=10,
|
|
866
|
+
)
|
|
867
|
+
if result.returncode == 0:
|
|
868
|
+
console.print(" [green]✓[/green] Docker daemon running")
|
|
869
|
+
else:
|
|
870
|
+
console.print(" [red]✗[/red] Docker daemon not running")
|
|
871
|
+
except Exception:
|
|
872
|
+
console.print(" [red]✗[/red] Cannot connect to Docker daemon")
|
|
873
|
+
|
|
874
|
+
# Check Compose version
|
|
875
|
+
try:
|
|
876
|
+
result = subprocess.run(
|
|
877
|
+
["docker", "compose", "version", "--short"],
|
|
878
|
+
capture_output=True,
|
|
879
|
+
text=True,
|
|
880
|
+
timeout=10,
|
|
881
|
+
)
|
|
882
|
+
if result.returncode == 0:
|
|
883
|
+
version = result.stdout.strip()
|
|
884
|
+
console.print(f" [green]✓[/green] Docker Compose: {version}")
|
|
885
|
+
else:
|
|
886
|
+
console.print(" [yellow]![/yellow] Docker Compose not available")
|
|
887
|
+
except Exception:
|
|
888
|
+
console.print(" [yellow]![/yellow] Docker Compose not found")
|
|
889
|
+
|
|
890
|
+
console.print()
|
|
891
|
+
|
|
892
|
+
# Test images
|
|
893
|
+
console.print("[bold]Image Architecture Test:[/bold]")
|
|
894
|
+
test_images = [
|
|
895
|
+
("python:3.12-slim", "multi-arch (nl2bash/cybashbench)"),
|
|
896
|
+
("cybench/cybench:latest", "x86-only (cybench/cybergym)"),
|
|
897
|
+
]
|
|
898
|
+
|
|
899
|
+
for image, description in test_images:
|
|
900
|
+
if check_image_exists_locally(image):
|
|
901
|
+
arch = get_image_architecture(image)
|
|
902
|
+
if arch:
|
|
903
|
+
if is_linux_arm and arch == "amd64":
|
|
904
|
+
console.print(
|
|
905
|
+
f" [yellow]![/yellow] {image}: {arch} [dim](x86 on ARM - needs QEMU)[/dim]"
|
|
906
|
+
)
|
|
907
|
+
else:
|
|
908
|
+
console.print(f" [green]✓[/green] {image}: {arch}")
|
|
909
|
+
else:
|
|
910
|
+
console.print(f" [dim]?[/dim] {image}: cached (unknown arch)")
|
|
911
|
+
else:
|
|
912
|
+
console.print(f" [dim]-[/dim] {image}: not cached - {description}")
|
|
913
|
+
|
|
914
|
+
console.print()
|
|
915
|
+
|
|
916
|
+
# QEMU check (for Linux ARM64)
|
|
917
|
+
if is_linux_arm:
|
|
918
|
+
console.print("[bold]QEMU Emulation:[/bold]")
|
|
919
|
+
try:
|
|
920
|
+
# Check if binfmt is set up for x86
|
|
921
|
+
result = subprocess.run(
|
|
922
|
+
["docker", "run", "--rm", "--platform", "linux/amd64", "alpine", "uname", "-m"],
|
|
923
|
+
capture_output=True,
|
|
924
|
+
text=True,
|
|
925
|
+
timeout=30,
|
|
926
|
+
)
|
|
927
|
+
if result.returncode == 0 and "x86_64" in result.stdout:
|
|
928
|
+
console.print(" [green]✓[/green] QEMU x86 emulation working")
|
|
929
|
+
else:
|
|
930
|
+
console.print(" [red]✗[/red] QEMU x86 emulation NOT working")
|
|
931
|
+
console.print()
|
|
932
|
+
console.print(" [yellow]To enable QEMU emulation, run:[/yellow]")
|
|
933
|
+
console.print(" [bold]docker run --privileged --rm tonistiigi/binfmt --install all[/bold]")
|
|
934
|
+
except subprocess.TimeoutExpired:
|
|
935
|
+
console.print(" [yellow]![/yellow] QEMU test timed out")
|
|
936
|
+
except Exception as e:
|
|
937
|
+
console.print(f" [red]✗[/red] QEMU test failed: {e}")
|
|
938
|
+
console.print()
|
|
939
|
+
|
|
940
|
+
console.print("[bold]Recommendation:[/bold]")
|
|
941
|
+
if is_linux_arm:
|
|
942
|
+
console.print(" For nl2bash/cybashbench: Should work with native ARM images")
|
|
943
|
+
console.print(" For CTF challenges (cybench, nyuctf, etc.): Requires QEMU emulation")
|
|
944
|
+
else:
|
|
945
|
+
console.print(" [green]✓[/green] Standard platform - all benchmarks should work")
|
|
946
|
+
|
|
947
|
+
console.print()
|
|
948
|
+
console.print("[dim]Run 'hte-cli session join <id>' to test with a real task[/dim]")
|
|
949
|
+
|
|
950
|
+
|
|
675
951
|
# =============================================================================
|
|
676
952
|
# Helper Functions
|
|
677
953
|
# =============================================================================
|
hte_cli/image_utils.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import platform
|
|
5
6
|
import pty
|
|
6
7
|
import re
|
|
7
8
|
import select
|
|
@@ -13,6 +14,119 @@ import yaml
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
# Architecture mapping: Python's platform.machine() -> Docker platform
|
|
18
|
+
ARCH_TO_DOCKER_PLATFORM = {
|
|
19
|
+
"x86_64": "linux/amd64",
|
|
20
|
+
"amd64": "linux/amd64",
|
|
21
|
+
"aarch64": "linux/arm64",
|
|
22
|
+
"arm64": "linux/arm64",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Docker image architecture names (from docker inspect)
|
|
26
|
+
DOCKER_ARCH_TO_PLATFORM = {
|
|
27
|
+
"amd64": "linux/amd64",
|
|
28
|
+
"arm64": "linux/arm64",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_host_architecture() -> str:
|
|
33
|
+
"""
|
|
34
|
+
Get the host machine's architecture.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Architecture string (e.g., "x86_64", "arm64", "aarch64")
|
|
38
|
+
"""
|
|
39
|
+
return platform.machine()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_host_docker_platform() -> str | None:
|
|
43
|
+
"""
|
|
44
|
+
Get the Docker platform string for the host architecture.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Docker platform (e.g., "linux/amd64", "linux/arm64") or None if unknown
|
|
48
|
+
"""
|
|
49
|
+
arch = get_host_architecture()
|
|
50
|
+
return ARCH_TO_DOCKER_PLATFORM.get(arch)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_image_architecture(image: str) -> str | None:
|
|
54
|
+
"""
|
|
55
|
+
Get the architecture of a locally cached Docker image.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
image: Image name (e.g., "python:3.12-slim")
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Architecture string (e.g., "amd64", "arm64") or None if not found/error
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
["docker", "image", "inspect", image, "--format", "{{.Architecture}}"],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
timeout=10,
|
|
69
|
+
)
|
|
70
|
+
if result.returncode == 0:
|
|
71
|
+
return result.stdout.strip()
|
|
72
|
+
return None
|
|
73
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_image_architecture_matches_host(image: str) -> tuple[bool, str | None, str | None]:
|
|
78
|
+
"""
|
|
79
|
+
Check if a cached image's architecture matches the host.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
image: Image name to check
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Tuple of (matches, image_arch, host_arch):
|
|
86
|
+
- matches: True if architectures match or image not cached
|
|
87
|
+
- image_arch: The cached image's architecture (None if not cached)
|
|
88
|
+
- host_arch: The host's architecture
|
|
89
|
+
"""
|
|
90
|
+
host_arch = get_host_architecture()
|
|
91
|
+
host_platform = get_host_docker_platform()
|
|
92
|
+
|
|
93
|
+
if not host_platform:
|
|
94
|
+
# Unknown host architecture - can't check
|
|
95
|
+
logger.warning(f"Unknown host architecture: {host_arch}")
|
|
96
|
+
return (True, None, host_arch)
|
|
97
|
+
|
|
98
|
+
image_arch = get_image_architecture(image)
|
|
99
|
+
if not image_arch:
|
|
100
|
+
# Image not cached - nothing to check
|
|
101
|
+
return (True, None, host_arch)
|
|
102
|
+
|
|
103
|
+
image_platform = DOCKER_ARCH_TO_PLATFORM.get(image_arch)
|
|
104
|
+
matches = image_platform == host_platform
|
|
105
|
+
|
|
106
|
+
if not matches:
|
|
107
|
+
logger.info(
|
|
108
|
+
f"Architecture mismatch for {image}: "
|
|
109
|
+
f"cached={image_arch} ({image_platform}), host={host_arch} ({host_platform})"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return (matches, image_arch, host_arch)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_running_in_linux_vm_on_arm() -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Detect if running Linux ARM64 (likely a VM on Apple Silicon).
|
|
118
|
+
|
|
119
|
+
This is a common setup that causes architecture issues because:
|
|
120
|
+
- macOS Docker Desktop handles multi-arch via Rosetta
|
|
121
|
+
- Linux ARM64 in a VM doesn't have that, needs explicit platform handling
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if running Linux on ARM64
|
|
125
|
+
"""
|
|
126
|
+
import sys
|
|
127
|
+
return sys.platform == "linux" and get_host_architecture() in ("aarch64", "arm64")
|
|
128
|
+
|
|
129
|
+
|
|
16
130
|
def extract_images_from_compose(compose_yaml: str) -> list[str]:
|
|
17
131
|
"""
|
|
18
132
|
Extract Docker image names from a compose.yaml string.
|
|
@@ -86,6 +200,79 @@ def check_image_exists_locally(image: str) -> bool:
|
|
|
86
200
|
return False
|
|
87
201
|
|
|
88
202
|
|
|
203
|
+
def remove_image(image: str) -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Remove a Docker image from local cache.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
image: Image name to remove
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
True if removed successfully, False otherwise
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
result = subprocess.run(
|
|
215
|
+
["docker", "rmi", image],
|
|
216
|
+
capture_output=True,
|
|
217
|
+
timeout=30,
|
|
218
|
+
)
|
|
219
|
+
return result.returncode == 0
|
|
220
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def fix_image_architecture(
|
|
225
|
+
image: str,
|
|
226
|
+
on_status: Callable[[str], None] | None = None,
|
|
227
|
+
) -> tuple[bool, str]:
|
|
228
|
+
"""
|
|
229
|
+
Check if a cached image has wrong architecture and fix it if needed.
|
|
230
|
+
|
|
231
|
+
For Linux ARM64 hosts (e.g., VM on Apple Silicon), this:
|
|
232
|
+
1. Checks if the cached image is amd64 when host is arm64
|
|
233
|
+
2. Removes the wrongly-cached image
|
|
234
|
+
3. Re-pulls with explicit --platform linux/arm64
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
image: Image name to check/fix
|
|
238
|
+
on_status: Callback for status updates
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Tuple of (needed_fix, message):
|
|
242
|
+
- needed_fix: True if image was re-pulled
|
|
243
|
+
- message: Description of what happened
|
|
244
|
+
"""
|
|
245
|
+
matches, image_arch, host_arch = check_image_architecture_matches_host(image)
|
|
246
|
+
|
|
247
|
+
if matches:
|
|
248
|
+
if image_arch:
|
|
249
|
+
return (False, f"architecture OK ({image_arch})")
|
|
250
|
+
else:
|
|
251
|
+
return (False, "not cached")
|
|
252
|
+
|
|
253
|
+
# Architecture mismatch detected
|
|
254
|
+
host_platform = get_host_docker_platform()
|
|
255
|
+
if not host_platform:
|
|
256
|
+
return (False, f"unknown host architecture: {host_arch}")
|
|
257
|
+
|
|
258
|
+
if on_status:
|
|
259
|
+
on_status(f"Cached image is {image_arch}, host is {host_arch} - re-pulling...")
|
|
260
|
+
|
|
261
|
+
# Remove the wrongly-cached image
|
|
262
|
+
logger.info(f"Removing wrongly-cached {image_arch} image: {image}")
|
|
263
|
+
if not remove_image(image):
|
|
264
|
+
return (False, f"failed to remove cached {image_arch} image")
|
|
265
|
+
|
|
266
|
+
# Re-pull with correct platform
|
|
267
|
+
logger.info(f"Re-pulling {image} with platform {host_platform}")
|
|
268
|
+
success = pull_image_with_progress(image, platform=host_platform)
|
|
269
|
+
|
|
270
|
+
if success:
|
|
271
|
+
return (True, f"re-pulled as {host_platform.split('/')[-1]}")
|
|
272
|
+
else:
|
|
273
|
+
return (False, f"failed to re-pull with platform {host_platform}")
|
|
274
|
+
|
|
275
|
+
|
|
89
276
|
def pull_image_with_progress(
|
|
90
277
|
image: str,
|
|
91
278
|
platform: str | None = None,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
hte_cli/__init__.py,sha256=fDGXp-r8bIoLtlQnn5xJ_CpwMhonvk9bGjZQsjA2mDI,914
|
|
2
2
|
hte_cli/__main__.py,sha256=63n0gNGfskidWDU0aAIF2N8lylVCLYKVIkrN9QiORoo,107
|
|
3
3
|
hte_cli/api_client.py,sha256=m42kfFZS72Nu_VuDwxRsLNy4ziCcvgk7KNWBh9gwqy0,9257
|
|
4
|
-
hte_cli/cli.py,sha256=
|
|
4
|
+
hte_cli/cli.py,sha256=KpDuJp4VRTfjyVG_ZK3zVCgdDD2qaNjtqBFDHlofUnE,38678
|
|
5
5
|
hte_cli/config.py,sha256=42Xv__YMSeRLs2zhGukJkIXFKtnBtYCHnONfViGyt2g,3387
|
|
6
6
|
hte_cli/errors.py,sha256=1J5PpxcUKBu6XjigMMCPOq4Zc12tnv8LhAsiaVFWLQM,2762
|
|
7
7
|
hte_cli/events.py,sha256=oDKCS-a0IZ7bz7xkwQj5eM4DoDCYvnclAGohrMTWf8s,5644
|
|
8
|
-
hte_cli/image_utils.py,sha256=
|
|
8
|
+
hte_cli/image_utils.py,sha256=n4AmbaR9tbH0ahXbTOn7Rr_VeRbhg1RgWknsWwI_83c,13249
|
|
9
9
|
hte_cli/runner.py,sha256=SWl9FF4X3e9eBbZyL0ujhmmSL5OK8J6st-Ty0jD5AWM,14550
|
|
10
10
|
hte_cli/scorers.py,sha256=B0ZjQ3Fh-VDkc_8CDc86yW7vpdimbV3RSqs7l-VeUIg,6629
|
|
11
11
|
hte_cli/version_check.py,sha256=WVZyGy2XfAghQYdd2N9-0Qfg-7pgp9gt4761-PnmacI,1708
|
|
12
|
-
hte_cli-0.2.
|
|
13
|
-
hte_cli-0.2.
|
|
14
|
-
hte_cli-0.2.
|
|
15
|
-
hte_cli-0.2.
|
|
12
|
+
hte_cli-0.2.29.dist-info/METADATA,sha256=upaON6sKmJaItR2khC6Sc2-9UFYu7wyuy9-yG2lq5MQ,3820
|
|
13
|
+
hte_cli-0.2.29.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
hte_cli-0.2.29.dist-info/entry_points.txt,sha256=XbyEEi1H14DFAt0Kdl22e_IRVEGzimSzYSh5HlhKlFA,41
|
|
15
|
+
hte_cli-0.2.29.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|