sima-cli 0.0.23__py3-none-any.whl → 0.0.25__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.
sima_cli/__version__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "0.0.23"
2
+ __version__ = "0.0.25"
sima_cli/cli.py CHANGED
@@ -235,9 +235,10 @@ def show_mla_memory_usage(ctx):
235
235
  @click.option("-b", "--boardtype", type=click.Choice(["modalix", "mlsoc"], case_sensitive=False), default="mlsoc", show_default=True, help="Target board type.")
236
236
  @click.option("-t", "--fwtype", type=click.Choice(["yocto", "elxr"], case_sensitive=False), default="yocto", show_default=True, help="Target firmware type.")
237
237
  @click.option("-n", "--netboot", is_flag=True, default=False, show_default=True, help="Prepare image for network boot and launch TFTP server.")
238
+ @click.option("-r", "--rootfs", required=False, help="Custom root fs folders (internal use only)")
238
239
  @click.option("-a", "--autoflash", is_flag=True, default=False, show_default=True, help="Net boot the DevKit and automatically flash the internal storage - TBD")
239
240
  @click.pass_context
240
- def bootimg_cmd(ctx, version, boardtype, netboot, autoflash, fwtype):
241
+ def bootimg_cmd(ctx, version, boardtype, netboot, autoflash, fwtype, rootfs):
241
242
  """
242
243
  Download and burn a removable media or setup TFTP boot.
243
244
 
@@ -261,11 +262,12 @@ def bootimg_cmd(ctx, version, boardtype, netboot, autoflash, fwtype):
261
262
  click.echo(f" 🔹 Board Type: {boardtype}")
262
263
  click.echo(f" 🔹 F/W Type : {fwtype}")
263
264
  click.echo(f" 🔹 F/W Flavor: headless")
265
+ click.echo(f" 🔹 Custom RootFS: {rootfs}")
264
266
 
265
267
  try:
266
268
  boardtype = boardtype if boardtype != 'mlsoc' else 'davinci'
267
269
  if netboot or autoflash:
268
- setup_netboot(version, boardtype, internal, autoflash, flavor='headless')
270
+ setup_netboot(version, boardtype, internal, autoflash, flavor='headless', rootfs=rootfs)
269
271
  click.echo("✅ Netboot image prepared and TFTP server is running.")
270
272
  else:
271
273
  write_image(version, boardtype, fwtype, internal, flavor='headless')
@@ -298,9 +300,9 @@ def install_cmd(ctx, component, version, mirror, tag):
298
300
 
299
301
  sima-cli install optiview
300
302
 
301
- sima-cli install -m https://example.com/packages/foo/metadata.json
303
+ sima-cli install -m https://custom-server/packages/foo/metadata.json
302
304
 
303
- sima-cli install examples.llima -v 1.7.0
305
+ sima-cli install samples/llima -v 1.7.0
304
306
  """
305
307
  internal = ctx.obj.get("internal", False)
306
308
 
@@ -398,9 +400,9 @@ def nvme_cmd(ctx, operation):
398
400
 
399
401
  Available operations:
400
402
 
401
- format - Format the NVMe drive and mount it to /mnt/nvme
403
+ format - Format the NVMe drive and mount it to /media/nvme
402
404
 
403
- remount - Remount the existing NVMe partition to /mnt/nvme
405
+ remount - Remount the existing NVMe partition to /media/nvme
404
406
 
405
407
  Example:
406
408
  sima-cli nvme format
sima_cli/storage/nvme.py CHANGED
@@ -35,7 +35,7 @@ def format_nvme(lbaf_index):
35
35
 
36
36
  def add_nvme_to_fstab():
37
37
  """
38
- Add UUID-based entry for /dev/nvme0n1p1 to /etc/fstab for persistent mounting at /mnt/nvme.
38
+ Add UUID-based entry for /dev/nvme0n1p1 to /etc/fstab for persistent mounting at /media/nvme.
39
39
  Only appends if the entry does not already exist.
40
40
  Requires root permission to run blkid and modify /etc/fstab.
41
41
  """
@@ -56,12 +56,12 @@ def add_nvme_to_fstab():
56
56
  fs_type = type_match.group(1)
57
57
 
58
58
  # Construct the fstab line
59
- fstab_entry = f"UUID={uuid} /mnt/nvme {fs_type} defaults,noatime,noauto 0 0"
59
+ fstab_entry = f"UUID={uuid} /media/nvme {fs_type} defaults,noatime 0 0"
60
60
 
61
61
  # Check if entry already exists
62
62
  with open(fstab_path, "r") as f:
63
63
  for line in f:
64
- if uuid in line or "/mnt/nvme" in line:
64
+ if uuid in line or "/media/nvme" in line:
65
65
  click.echo("ℹ️ NVMe UUID or mount point already exists in /etc/fstab.")
66
66
  return
67
67
 
@@ -78,19 +78,19 @@ def add_nvme_to_fstab():
78
78
  def mount_nvme():
79
79
  try:
80
80
  # Create mount point
81
- subprocess.run("sudo mkdir -p /mnt/nvme", shell=True, check=True)
81
+ subprocess.run("sudo mkdir -p /media/nvme", shell=True, check=True)
82
82
 
83
83
  # Mount the NVMe partition
84
- subprocess.run("sudo mount /dev/nvme0n1p1 /mnt/nvme", shell=True, check=True)
84
+ subprocess.run("sudo mount /dev/nvme0n1p1 /media/nvme", shell=True, check=True)
85
85
 
86
86
  add_nvme_to_fstab()
87
87
 
88
88
  subprocess.run("sudo mount -a", shell=True, check=True)
89
89
 
90
90
  # Change ownership to user 'sima'
91
- subprocess.run("sudo chown sima:sima /mnt/nvme", shell=True, check=True)
91
+ subprocess.run("sudo chown sima:sima /media/nvme", shell=True, check=True)
92
92
 
93
- subprocess.run("sudo chmod 755 /mnt/nvme", shell=True, check=True)
93
+ subprocess.run("sudo chmod 755 /media/nvme", shell=True, check=True)
94
94
 
95
95
 
96
96
  print("✅ NVMe mounted and write permission granted to user 'sima'.")
@@ -99,10 +99,6 @@ def mount_nvme():
99
99
  print(f"❌ Error during NVMe mount: {e}")
100
100
 
101
101
  def nvme_format():
102
- if not is_modalix_devkit():
103
- click.echo("❌ This command can only be run on the Modalix DevKit.")
104
- return
105
-
106
102
  nvme_info = scan_nvme()
107
103
  if not nvme_info:
108
104
  click.echo("❌ No NVMe drive detected.")
@@ -121,12 +117,12 @@ def nvme_format():
121
117
 
122
118
  try:
123
119
  # Unmount before formatting, ignore error if not mounted
124
- subprocess.run("sudo umount /mnt/nvme", shell=True, check=False)
120
+ subprocess.run("sudo umount /media/nvme", shell=True, check=False)
125
121
 
126
122
  # Format and mount
127
123
  format_nvme(lbaf_index)
128
124
  mount_nvme()
129
- click.echo("✅ NVMe drive formatted and mounted at /mnt/nvme.")
125
+ click.echo("✅ NVMe drive formatted and mounted at /media/nvme.")
130
126
  except subprocess.CalledProcessError:
131
127
  click.echo("❌ Formatting process failed.")
132
128
 
@@ -22,23 +22,46 @@ def find_mkfs_ext4() -> str:
22
22
  return None
23
23
 
24
24
 
25
+ def wipe_existing_partitions(device_path: str):
26
+ """
27
+ Fully wipe all partition entries using sgdisk or dd to ensure a clean disk.
28
+ """
29
+ click.echo(f"💣 Wiping partition table on {device_path}")
30
+ subprocess.run(["sudo", "sgdisk", "--zap-all", device_path], check=False)
31
+ subprocess.run(["sudo", "wipefs", "--all", device_path], check=False)
32
+
33
+ # Optional: clear first 1MB and last 1MB
34
+ subprocess.run(["sudo", "dd", "if=/dev/zero", f"of={device_path}", "bs=1M", "count=1"], check=False)
35
+ subprocess.run(["sudo", "blockdev", "--rereadpt", device_path], check=False)
36
+
25
37
  def kill_partition_users(device_path: str):
26
- """Kill processes using the partitions on the device"""
38
+ """
39
+ Unmount all mounted partitions of the device, and kill only real user processes if needed.
40
+ """
27
41
  try:
42
+ # List all partitions of the device (e.g., /dev/sda1, /dev/sda2)
28
43
  output = subprocess.check_output(["lsblk", "-n", "-o", "NAME", device_path]).decode().strip().splitlines()
29
44
  partitions = [f"/dev/{line.strip()}" for line in output if line.strip() and f"/dev/{line.strip()}" != device_path]
30
-
31
- for p in partitions:
45
+
46
+ for part in partitions:
47
+ # Check if the partition is mounted
48
+ mountpoint = subprocess.run(["findmnt", "-n", "-o", "TARGET", part],
49
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
50
+ if mountpoint.returncode == 0 and mountpoint.stdout.strip():
51
+ # It's mounted — unmount it
52
+ click.echo(f"🛑 Unmounting mounted partition {part}")
53
+ subprocess.run(["sudo", "umount", part], check=False)
54
+
55
+ # Optionally kill any real user-space processes using the partition
32
56
  try:
33
- users = subprocess.check_output(["sudo", "lsof", p], stderr=subprocess.DEVNULL).decode().splitlines()
57
+ users = subprocess.check_output(["sudo", "lsof", part], stderr=subprocess.DEVNULL).decode().splitlines()
34
58
  pids = {line.split()[1] for line in users[1:] if line.strip()}
35
59
  for pid in pids:
60
+ click.echo(f"🔪 Killing PID {pid} using {part}")
36
61
  subprocess.run(["sudo", "kill", "-9", pid], check=False)
37
62
  except subprocess.CalledProcessError:
38
- # lsof exits 1 if no process is using the file
39
- continue
63
+ pass
40
64
 
41
- # Let the system settle
42
65
  time.sleep(1)
43
66
 
44
67
  except Exception as e:
@@ -121,6 +144,7 @@ def sdcard_format():
121
144
  unmount_device(selected_path)
122
145
  force_release_device(selected_path)
123
146
  kill_partition_users(selected_path)
147
+ wipe_existing_partitions(selected_path)
124
148
 
125
149
  try:
126
150
  create_partition_table(selected_path)
sima_cli/update/local.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import os
2
- import subprocess
3
- import tempfile
4
- from typing import List, Tuple
2
+ from typing import Tuple
5
3
  import pty
6
4
  import click
7
5
 
6
+ from sima_cli.utils.env import is_board_running_full_image, get_exact_devkit_type
7
+
8
8
  def _run_local_cmd(command: str, passwd: str) -> bool:
9
9
  """
10
10
  Run a local command using a pseudo-terminal (pty) to force live output flushing,
@@ -44,12 +44,12 @@ def _run_local_cmd(command: str, passwd: str) -> bool:
44
44
  return False
45
45
 
46
46
 
47
- def get_local_board_info() -> Tuple[str, str]:
47
+ def get_local_board_info() -> Tuple[str, str, bool]:
48
48
  """
49
49
  Retrieve the local board type and build version by reading /etc/build or /etc/buildinfo.
50
50
 
51
51
  Returns:
52
- (board_type, build_version): Tuple of strings, or ('', '') on failure.
52
+ (board_type, build_version, fdt_name, full_image): Tuple of strings, or ('', '') on failure.
53
53
  """
54
54
  board_type = ""
55
55
  build_version = ""
@@ -70,7 +70,9 @@ def get_local_board_info() -> Tuple[str, str]:
70
70
  except Exception:
71
71
  continue
72
72
 
73
- return board_type, build_version
73
+ fdt_name = get_exact_devkit_type()
74
+
75
+ return board_type, build_version, fdt_name, is_board_running_full_image()
74
76
 
75
77
 
76
78
  def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str, flavor: str):
@@ -20,6 +20,7 @@ SOCK_TIMEOUT = 2 # Timeout for faster retransmits
20
20
 
21
21
  log = logging.getLogger("tftpy.InteractiveTftpServer")
22
22
  emmc_image_paths = []
23
+ custom_rootfs = ''
23
24
 
24
25
  def flash_emmc(client_manager, emmc_image_paths):
25
26
  """Flash eMMC on a selected client device."""
@@ -127,7 +128,7 @@ class ClientManager:
127
128
 
128
129
  while not self.shutdown_event.is_set():
129
130
  try:
130
- board_type, build_version, _ = get_remote_board_info(ip)
131
+ board_type, build_version, _, _ = get_remote_board_info(ip)
131
132
 
132
133
  if board_type and build_version:
133
134
  with self.lock:
@@ -346,7 +347,7 @@ def run_cli(client_manager):
346
347
  click.echo("\n🛑 Exiting netboot session.")
347
348
  return True
348
349
 
349
- def setup_netboot(version: str, board: str, internal: bool = False, autoflash: bool = False, flavor: str = 'headless'):
350
+ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: bool = False, flavor: str = 'headless', rootfs: str = ''):
350
351
  """
351
352
  Download and serve a bootable image for network boot over TFTP with client monitoring.
352
353
 
@@ -356,11 +357,13 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
356
357
  internal (bool): Whether to use internal download sources. Defaults to False.
357
358
  autoflash (bool): Whether to automatically flash the devkit when networked booted. Defaults to False.
358
359
  flavor (str): The software flavor, can be either headless or full.
360
+ rootfs (str): The root fs folder, which contains the .wic.gz file and the .bmap file, for custom image writing.
359
361
 
360
362
  Raises:
361
363
  RuntimeError: If the download or TFTP setup fails.
362
364
  """
363
365
  global emmc_image_paths
366
+ global custom_rootfs
364
367
 
365
368
  if platform.system() == "Windows":
366
369
  click.echo("❌ Netboot with built-in TFTP is not supported on Windows. Use macOS or Linux.")
@@ -373,10 +376,30 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
373
376
  raise ValueError("Expected list of extracted files, got something else.")
374
377
  extract_dir = os.path.dirname(file_list[0])
375
378
  click.echo(f"📁 Image extracted to: {extract_dir}")
379
+
376
380
  # Extract specific image paths
377
381
  wic_gz_file = next((f for f in file_list if f.endswith(".wic.gz")), None)
378
382
  bmap_file = next((f for f in file_list if f.endswith(".wic.bmap")), None)
379
383
  emmc_image_paths = [p for p in [wic_gz_file, bmap_file] if p]
384
+
385
+ # Check global custom_rootfs before doing anything else
386
+ custom_rootfs = rootfs
387
+ if custom_rootfs:
388
+ if not os.path.isdir(custom_rootfs):
389
+ raise RuntimeError(f"❌ custom_rootfs path is not a directory: {custom_rootfs}")
390
+
391
+ import glob
392
+ wic_gz_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.wic.gz"))), None)
393
+ bmap_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.wic.bmap"))), None)
394
+
395
+ if not (wic_gz_file and bmap_file):
396
+ raise RuntimeError(
397
+ f"❌ custom_rootfs '{custom_rootfs}' must contain both .wic.gz and .wic.bmap files."
398
+ )
399
+
400
+ emmc_image_paths = [wic_gz_file, bmap_file]
401
+ click.echo(f"📁 Using custom_rootfs: {custom_rootfs}")
402
+
380
403
  click.echo(f"📁 eMMC image paths are: {emmc_image_paths}")
381
404
 
382
405
  except Exception as e:
sima_cli/update/remote.py CHANGED
@@ -59,11 +59,12 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
59
59
  ip (str): IP address of the board.
60
60
 
61
61
  Returns:
62
- (board_type, build_version, fdt_name): Tuple of strings, or ('', '', '') on failure.
62
+ (board_type, build_version, fdt_name, full_image): Tuple of strings, or ('', '', '') on failure.
63
63
  """
64
64
  board_type = ""
65
65
  build_version = ""
66
66
  fdt_name = ""
67
+ full_image = False
67
68
 
68
69
  try:
69
70
  ssh = paramiko.SSHClient()
@@ -71,13 +72,20 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
71
72
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
72
73
 
73
74
  # Retrieve build info
74
- stdin, stdout, stderr = ssh.exec_command("cat /etc/build 2>/dev/null || cat /etc/buildinfo 2>/dev/null")
75
+ _, stdout, _ = ssh.exec_command("cat /etc/build 2>/dev/null || cat /etc/buildinfo 2>/dev/null")
75
76
  build_output = stdout.read().decode()
76
77
 
77
78
  # Retrieve fdt_name from fw_printenv
78
- stdin, stdout, stderr = ssh.exec_command("fw_printenv fdt_name 2>/dev/null")
79
+ _, stdout, _ = ssh.exec_command("fw_printenv fdt_name 2>/dev/null")
79
80
  fdt_output = stdout.read().decode()
80
81
 
82
+ # 3) NVMe presence (ensure sbin is in PATH for non-root shells)
83
+ # Note: 'command -v' is POSIX; 'which' as fallback on some busybox setups.
84
+ nvme_check_cmd = r'PATH="$PATH:/usr/sbin:/sbin"; command -v nvme >/dev/null 2>&1 || which nvme >/dev/null 2>&1; echo $?'
85
+ _, stdout, _ = ssh.exec_command(nvme_check_cmd)
86
+ nvme_rc = stdout.read().decode().strip()
87
+ full_image = (nvme_rc == "0")
88
+
81
89
  ssh.close()
82
90
 
83
91
  for line in build_output.splitlines():
@@ -91,11 +99,11 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
91
99
  if line.startswith("fdt_name"):
92
100
  fdt_name = line.split("=", 1)[-1].strip()
93
101
 
94
- return board_type, build_version, fdt_name
102
+ return board_type, build_version, fdt_name, full_image
95
103
 
96
104
  except Exception as e:
97
105
  click.echo(f"Unable to retrieve board info with error: {e}, board may be still booting.")
98
- return "", "", ""
106
+ return "", "", "", False
99
107
 
100
108
 
101
109
  def _scp_file(sftp, local_path: str, remote_path: str):
@@ -71,6 +71,35 @@ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = Fals
71
71
 
72
72
  return download_url
73
73
 
74
+ def _confirm_flavor_switching(full_image: bool, flavor: str) -> str:
75
+ """
76
+ Check if the system is running a different flavor from the board and prompt user to confirm switching.
77
+
78
+ Args:
79
+ full_image (bool): Indicates if the current image is full
80
+ flavor (str): The desired flavor of the image ('full' or 'headless')
81
+
82
+ Returns:
83
+ str: The flavor to use ('full' or 'headless')
84
+ """
85
+ if (full_image and flavor != 'full') or (not full_image and flavor == 'full'):
86
+ click.echo(f"🔄 The current image running on the board has a different flavor from what you specified ({flavor}).")
87
+ click.echo("Please choose an option:")
88
+ choice = click.prompt(
89
+ f" a) Switch to the specified {flavor} flavor\n b) Keep the existing flavor\n",
90
+ type=click.Choice(['a', 'b'], case_sensitive=False),
91
+ default='a',
92
+ show_choices=False # Choices are already shown in the prompt
93
+ )
94
+
95
+ if choice.lower() == 'b':
96
+ flavor = 'full' if full_image else 'headless'
97
+ click.echo(f"🔄 Keeping the existing flavor: {flavor}")
98
+ else:
99
+ click.echo(f"🔄 Switching to the specified flavor: {flavor}")
100
+
101
+ return flavor
102
+
74
103
  def _pick_from_available_versions(board: str, version_or_url: str, internal: bool, flavor: str, swtype: str) -> str:
75
104
  """
76
105
  Presents an interactive menu (with search) for selecting a firmware version.
@@ -236,6 +265,7 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
236
265
 
237
266
  if not extracted_paths:
238
267
  click.echo("⚠️ No matching files were found or extracted.")
268
+ exit()
239
269
 
240
270
  return extracted_paths
241
271
 
@@ -373,11 +403,13 @@ def _update_board(extracted_paths: List[str], board: str, passwd: str, flavor: s
373
403
  return
374
404
 
375
405
  # Optionally verify the board type
376
- board_type, _ = get_local_board_info()
406
+ board_type, _, _, full_image = get_local_board_info()
377
407
  if board_type.lower() != board.lower():
378
408
  click.echo(f"❌ Board mismatch: expected '{board}', but found '{board_type}'")
379
409
  return
380
410
 
411
+ flavor = _confirm_flavor_switching(full_image=full_image, flavor=flavor)
412
+
381
413
  click.echo("✅ Board verified. Starting update...")
382
414
  push_and_update_local_board(troot_path, palette_path, passwd, flavor)
383
415
 
@@ -408,7 +440,7 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
408
440
 
409
441
  # Get remote board info
410
442
  click.echo("🔍 Checking remote board type and version...")
411
- remote_board, remote_version, fdt_name = get_remote_board_info(ip, passwd)
443
+ remote_board, remote_version, fdt_name, full_image = get_remote_board_info(ip, passwd)
412
444
 
413
445
  if not remote_board:
414
446
  click.echo("❌ Could not determine remote board type.")
@@ -472,12 +504,14 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
472
504
  click.echo(f"🔧 Requested version or URL: {version_or_url}, with flavor {flavor}")
473
505
 
474
506
  if env_type == 'board':
475
- board, version = get_local_board_info()
507
+ board, version, fdt_name, full_image = get_local_board_info()
476
508
  else:
477
- board, version, fdt_name = get_remote_board_info(ip, passwd)
509
+ board, version, fdt_name, full_image = get_remote_board_info(ip, passwd)
510
+
511
+ flavor = _confirm_flavor_switching(full_image=full_image, flavor=flavor)
478
512
 
479
513
  if board in ['davinci', 'modalix']:
480
- click.echo(f"🔧 Target board: {board} {fdt_name}, board currently running: {version}")
514
+ click.echo(f"🔧 Target board: {board} {fdt_name}, board currently running: {version}, full_image: {full_image}")
481
515
 
482
516
  if flavor == 'full' and fdt_name != 'modalix-som.dtb':
483
517
  click.echo(f"❌ You've requested updating {fdt_name} to full image, this is only supported for the Modalix DevKit")
@@ -489,7 +523,7 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
489
523
  flavor = 'headless'
490
524
 
491
525
  if 'http' not in version_or_url and not os.path.exists(version_or_url):
492
- version_or_url = _pick_from_available_versions(board, version_or_url, internal, flavor=flavor)
526
+ version_or_url = _pick_from_available_versions(board, version_or_url, internal, flavor=flavor, swtype='yocto')
493
527
 
494
528
  extracted_paths = _download_image(version_or_url, board, internal, flavor=flavor)
495
529
 
sima_cli/utils/env.py CHANGED
@@ -116,6 +116,37 @@ def get_exact_devkit_type() -> str:
116
116
 
117
117
  return ""
118
118
 
119
+ def is_board_running_full_image() -> bool:
120
+ """
121
+ Heuristic: return True if the 'full' image appears to be installed.
122
+ We detect this by checking for the NVMe CLI ('nvme'), which is bundled
123
+ with the full image but not the headless image.
124
+
125
+ Returns:
126
+ bool: True if nvme binary is found, else False.
127
+ """
128
+ try:
129
+ # Ensure sbin dirs are in search path (non-root shells often miss these)
130
+ search_path = os.environ.get("PATH", "")
131
+ sbin = "/usr/sbin:/sbin"
132
+ if search_path:
133
+ search_path = f"{search_path}:{sbin}"
134
+ else:
135
+ search_path = sbin
136
+
137
+ nvme_path = shutil.which("nvme", path=search_path)
138
+ if nvme_path and os.path.exists(nvme_path):
139
+ return True
140
+
141
+ # Fallback: direct checks (in case PATH is unusual)
142
+ for p in ("/usr/sbin/nvme", "/sbin/nvme", "/usr/bin/nvme", "/bin/nvme"):
143
+ if os.path.isfile(p) and os.access(p, os.X_OK):
144
+ return True
145
+
146
+ return False
147
+ except Exception:
148
+ return False
149
+
119
150
  def is_palette_sdk() -> bool:
120
151
  """
121
152
  Check if the environment is running inside the Palette SDK container.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 0.0.23
3
+ Version: 0.0.25
4
4
  Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
5
5
  Home-page: https://developer.sima.ai/
6
6
  Author: SiMa.ai
@@ -1,7 +1,7 @@
1
1
  sima_cli/__init__.py,sha256=Nb2jSg9-CX1XvSc1c21U9qQ3atINxphuNkNfmR-9P3o,332
2
2
  sima_cli/__main__.py,sha256=ehzD6AZ7zGytC2gLSvaJatxeD0jJdaEvNJvwYeGsWOg,69
3
- sima_cli/__version__.py,sha256=uP54joL5aVt8APefW7CPeBCn_PlfOZtxwbefmF7sYaI,49
4
- sima_cli/cli.py,sha256=crfrcrBW-SsxtVJFGCDTU42Z342Yk_Isp0gynjKmc3c,16899
3
+ sima_cli/__version__.py,sha256=R5i7Gn8i8_CnLil8QLKcejGs0ESjVKw6151FmEBxzqU,49
4
+ sima_cli/cli.py,sha256=3tTXP7n9891OL5Rz-ytMdftBTqhhJg2VA-riWRA6Kfo,17077
5
5
  sima_cli/app_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  sima_cli/app_zoo/app.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sima_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -26,25 +26,25 @@ sima_cli/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  sima_cli/sdk/syscheck.py,sha256=h9zCULW67y4i2hqiGc-hc1ucBDShA5FAe9NxwBGq-fM,4575
27
27
  sima_cli/serial/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  sima_cli/serial/serial.py,sha256=6xRta_PzE_DmmooYq35lbK76TYpAny5SEJAdYC_3fH0,4141
29
- sima_cli/storage/nvme.py,sha256=WHeNoz0r8vQwtcrcLSf8IQsj4rc_eVt8KlxhX0wwk3M,4855
30
- sima_cli/storage/sdcard.py,sha256=7X0r80U87YmJorV5CHLVzvXFtZcj6Agjv4y5M2UOKiQ,5031
29
+ sima_cli/storage/nvme.py,sha256=cCzYWcyPwcFu5pSMBkovsS4EwovaIMGolhEFStogXMA,4739
30
+ sima_cli/storage/sdcard.py,sha256=-WULjdV31-n8v5OOqfxR77qBbIK4hJnrD3xWxUVMoGI,6324
31
31
  sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,66
32
32
  sima_cli/update/bmaptool.py,sha256=KrhUGShBwY4Wzz50QiuMYAxxPgEy1nz5C68G-0a4qF4,4988
33
33
  sima_cli/update/bootimg.py,sha256=Eg8ZSp8LMZXbOMxX4ZPCjFOg3YEufmsVfojKrRc3fug,13631
34
- sima_cli/update/local.py,sha256=Blje7O2pcBopBLXwuVI826lnjPMTJ3lPU85dTUWUV48,3445
35
- sima_cli/update/netboot.py,sha256=RqFgBhixcjPEwdVGvKhR0TeztoFnmGigmXlA71WVksA,18647
34
+ sima_cli/update/local.py,sha256=no3PDChERbBcyjeNVAMR4dH4OaMoRUv8hpym-aoFdhQ,3597
35
+ sima_cli/update/netboot.py,sha256=hsJQLq4HVwFFkaWjA54VZdkMGDhO0RmylciS78qAfrM,19663
36
36
  sima_cli/update/query.py,sha256=b6Su7OlBGooIDcpb3_xH55tqkzznWcd_Fg1MkaNR874,4680
37
- sima_cli/update/remote.py,sha256=-AACggSRpReMI1Pw37wLJK5qm_Gfin-5jGiw5ggQWrU,11008
38
- sima_cli/update/updater.py,sha256=YXPfMdV29H3pu5bmLe-B3hpn3nQWlL5bwNytsN52Ces,22263
37
+ sima_cli/update/remote.py,sha256=ieZpKN2PCMu90Q72PnN66DZYyqpvBjQYHiDIjvdS5BY,11475
38
+ sima_cli/update/updater.py,sha256=-NXrGbPaxsj69NjdG_1GTB6g2bZMzk8sHJEg4NaKSgQ,23807
39
39
  sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
40
  sima_cli/utils/artifactory.py,sha256=6YyVpzVm8ATy7NEwT9nkWx-wptkXrvG7Wl_zDT6jmLs,2390
41
41
  sima_cli/utils/config.py,sha256=wE-cPQqY_gOqaP8t01xsRHD9tBUGk9MgBUm2GYYxI3E,1616
42
42
  sima_cli/utils/config_loader.py,sha256=7I5we1yiCai18j9R9jvhfUzAmT3OjAqVK35XSLuUw8c,2005
43
43
  sima_cli/utils/disk.py,sha256=66Kr631yhc_ny19up2aijfycWfD35AeLQOJgUsuH2hY,446
44
- sima_cli/utils/env.py,sha256=bNushG2BD243fNlqCpuUJxLF76inRxTFeSDkl_KCHy0,7130
44
+ sima_cli/utils/env.py,sha256=IP5HrH0lE7RMSiBeXcEt5GCLMT5p-QQroG-uGzl5XFU,8181
45
45
  sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
46
46
  sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
47
- sima_cli-0.0.23.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
47
+ sima_cli-0.0.25.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
48
48
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -53,8 +53,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
53
53
  tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
55
  tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
- sima_cli-0.0.23.dist-info/METADATA,sha256=jE_xgF2mw2x2Gxqt_K7lnukeFEtSTE-Z_6dvJBrixlk,3705
57
- sima_cli-0.0.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- sima_cli-0.0.23.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
59
- sima_cli-0.0.23.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
60
- sima_cli-0.0.23.dist-info/RECORD,,
56
+ sima_cli-0.0.25.dist-info/METADATA,sha256=2_wrl0eMxkscHwmaKn2XgMlPQC8dY76KGBSjL5-gT70,3705
57
+ sima_cli-0.0.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ sima_cli-0.0.25.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
59
+ sima_cli-0.0.25.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
60
+ sima_cli-0.0.25.dist-info/RECORD,,