hte-cli 0.2.27__py3-none-any.whl → 0.2.28__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 +86 -8
- hte_cli/image_utils.py +187 -0
- {hte_cli-0.2.27.dist-info → hte_cli-0.2.28.dist-info}/METADATA +1 -1
- {hte_cli-0.2.27.dist-info → hte_cli-0.2.28.dist-info}/RECORD +6 -6
- {hte_cli-0.2.27.dist-info → hte_cli-0.2.28.dist-info}/WHEEL +0 -0
- {hte_cli-0.2.27.dist-info → hte_cli-0.2.28.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,6 +328,22 @@ 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 = {}
|
|
@@ -334,13 +354,57 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
334
354
|
|
|
335
355
|
# Check if already cached
|
|
336
356
|
if check_image_exists_locally(img):
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
357
|
+
# Verify architecture matches host (important for Linux ARM64)
|
|
358
|
+
matches, image_arch, host_arch = check_image_architecture_matches_host(img)
|
|
359
|
+
|
|
360
|
+
if matches:
|
|
361
|
+
# Show architecture info on Linux ARM64 for transparency
|
|
362
|
+
if is_linux_arm and image_arch:
|
|
363
|
+
console.print(
|
|
364
|
+
f" [green]✓[/green] {short_name} [dim](cached, arch: {image_arch})[/dim]"
|
|
365
|
+
)
|
|
366
|
+
else:
|
|
367
|
+
console.print(f" [green]✓[/green] {short_name} [dim](cached)[/dim]")
|
|
368
|
+
cached_images.append(img)
|
|
369
|
+
continue
|
|
370
|
+
else:
|
|
371
|
+
# Architecture mismatch detected - this is the key fix for Linux ARM64
|
|
372
|
+
console.print(
|
|
373
|
+
f" [yellow]⚠[/yellow] {short_name} [yellow]architecture mismatch![/yellow]"
|
|
374
|
+
)
|
|
375
|
+
console.print(
|
|
376
|
+
f" [dim]Cached image: {image_arch} | Host: {host_arch}[/dim]"
|
|
377
|
+
)
|
|
378
|
+
console.print(
|
|
379
|
+
f" [dim]Removing cached image and re-pulling correct architecture...[/dim]"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
needed_fix, fix_msg = fix_image_architecture(img)
|
|
383
|
+
if needed_fix:
|
|
384
|
+
console.print(
|
|
385
|
+
f" [green]✓[/green] {short_name} [green]fixed![/green] [dim]({fix_msg})[/dim]"
|
|
386
|
+
)
|
|
387
|
+
pulled_images.append(img)
|
|
388
|
+
continue
|
|
389
|
+
else:
|
|
390
|
+
console.print(f" [red]✗[/red] {short_name} [dim]({fix_msg})[/dim]")
|
|
391
|
+
failed_images.append(img)
|
|
392
|
+
pull_errors[img] = fix_msg
|
|
393
|
+
continue
|
|
340
394
|
|
|
341
395
|
# Need to pull - show progress
|
|
342
396
|
last_status = ["connecting..."]
|
|
343
397
|
last_error = [""]
|
|
398
|
+
|
|
399
|
+
# On Linux ARM64, use host platform if no explicit platform in compose
|
|
400
|
+
# This prevents pulling amd64 images that won't run
|
|
401
|
+
pull_platform = platform
|
|
402
|
+
if not pull_platform and is_linux_arm and host_platform:
|
|
403
|
+
pull_platform = host_platform
|
|
404
|
+
console.print(
|
|
405
|
+
f" [dim]Pulling {short_name} with platform {host_platform}...[/dim]"
|
|
406
|
+
)
|
|
407
|
+
|
|
344
408
|
with console.status(
|
|
345
409
|
f"[yellow]↓[/yellow] {short_name} [dim]connecting...[/dim]"
|
|
346
410
|
) as status:
|
|
@@ -364,14 +428,21 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
364
428
|
last_error[0] = line
|
|
365
429
|
|
|
366
430
|
success = pull_image_with_progress(
|
|
367
|
-
img, platform=
|
|
431
|
+
img, platform=pull_platform, on_progress=show_progress
|
|
368
432
|
)
|
|
369
433
|
|
|
370
434
|
if success:
|
|
371
|
-
|
|
435
|
+
# Show platform info on Linux ARM64 for confirmation
|
|
436
|
+
if is_linux_arm and pull_platform:
|
|
437
|
+
arch_short = pull_platform.split("/")[-1] # e.g., "arm64"
|
|
438
|
+
console.print(
|
|
439
|
+
f" [green]✓[/green] {short_name} [dim](downloaded, arch: {arch_short})[/dim]"
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
console.print(f" [green]✓[/green] {short_name} [dim](downloaded)[/dim]")
|
|
372
443
|
pulled_images.append(img)
|
|
373
444
|
else:
|
|
374
|
-
platform_note = f" (platform: {
|
|
445
|
+
platform_note = f" (platform: {pull_platform})" if pull_platform else ""
|
|
375
446
|
console.print(f" [red]✗[/red] {short_name}{platform_note} [dim](failed)[/dim]")
|
|
376
447
|
if last_error[0]:
|
|
377
448
|
console.print(f" [dim]{last_error[0][:60]}[/dim]")
|
|
@@ -395,8 +466,15 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
395
466
|
console.print()
|
|
396
467
|
console.print("[yellow]Troubleshooting:[/yellow]")
|
|
397
468
|
console.print(" 1. Check Docker is running: docker info")
|
|
398
|
-
|
|
399
|
-
|
|
469
|
+
|
|
470
|
+
# Architecture-specific advice
|
|
471
|
+
if is_linux_arm:
|
|
472
|
+
console.print(f" 2. You're on Linux ARM64 - try: docker pull <image> --platform linux/arm64")
|
|
473
|
+
console.print(" 3. For x86-only images, enable QEMU: docker run --privileged --rm tonistiigi/binfmt --install all")
|
|
474
|
+
else:
|
|
475
|
+
console.print(" 2. Try manual pull: docker pull <image>")
|
|
476
|
+
|
|
477
|
+
console.print(" 4. Check network connectivity")
|
|
400
478
|
console.print()
|
|
401
479
|
console.print("Session remains active - you can retry with: hte-cli session join " + session_id)
|
|
402
480
|
sys.exit(1)
|
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=EgL9nlQ2R0TSp8qUtPe5YwTN3KlrNCQ8tRQnUhnFrP4,30647
|
|
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.28.dist-info/METADATA,sha256=cBb8EnxzFdZrsodJ3icqvprVywpeYJDb1__AI6Lm99A,3820
|
|
13
|
+
hte_cli-0.2.28.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
hte_cli-0.2.28.dist-info/entry_points.txt,sha256=XbyEEi1H14DFAt0Kdl22e_IRVEGzimSzYSh5HlhKlFA,41
|
|
15
|
+
hte_cli-0.2.28.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|