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 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
- console.print(f" [green]✓[/green] {short_name} [dim](cached)[/dim]")
338
- cached_images.append(img)
339
- continue
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=platform, on_progress=show_progress
431
+ img, platform=pull_platform, on_progress=show_progress
368
432
  )
369
433
 
370
434
  if success:
371
- console.print(f" [green]✓[/green] {short_name} [dim](downloaded)[/dim]")
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: {platform})" if platform else ""
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
- console.print(" 2. Try manual pull: docker pull python:3.12-slim --platform linux/amd64")
399
- console.print(" 3. Check network connectivity")
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hte-cli
3
- Version: 0.2.27
3
+ Version: 0.2.28
4
4
  Summary: Human Time-to-Completion Evaluation CLI
5
5
  Project-URL: Homepage, https://github.com/sean-peters-au/lyptus-mono
6
6
  Author: Lyptus Research
@@ -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=MSzswNhW3Pp2TUneIvoqsrdvkxIdMYNm5_uDUKFEcG0,26728
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=nVHhUY-QZ4uPpGSx3ByOiVGOnm9T11p_cVlb39FQb_Y,7717
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.27.dist-info/METADATA,sha256=ault1aIoseAbJzWU9TcAjkqTggxOUyO5w_DUaX1lUTs,3820
13
- hte_cli-0.2.27.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- hte_cli-0.2.27.dist-info/entry_points.txt,sha256=XbyEEi1H14DFAt0Kdl22e_IRVEGzimSzYSh5HlhKlFA,41
15
- hte_cli-0.2.27.dist-info/RECORD,,
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,,