sima-cli 2.1.11__tar.gz → 2.1.12__tar.gz

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 (149) hide show
  1. {sima_cli-2.1.11/sima_cli.egg-info → sima_cli-2.1.12}/PKG-INFO +1 -1
  2. {sima_cli-2.1.11 → sima_cli-2.1.12}/pyproject.toml +1 -1
  3. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/__version__.py +1 -1
  4. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/auth/auth0.py +12 -7
  5. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/auth/devportal.py +10 -1
  6. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/cli.py +25 -4
  7. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/compatibility.py +57 -5
  8. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/metadata_installer.py +153 -51
  9. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/metadata_validator.py +40 -0
  10. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/package_builder.py +2 -0
  11. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/registry.py +12 -1
  12. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/commands.py +165 -2
  13. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/install.py +52 -6
  14. sima_cli-2.1.12/sima_cli/sdk/linux_devkit_network.py +3 -0
  15. sima_cli-2.1.12/sima_cli/sdk/linux_shared_network.py +1425 -0
  16. sima_cli-2.1.12/sima_cli/sdk/network_doctor.py +920 -0
  17. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/preinstall.py +47 -18
  18. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/utils.py +12 -1
  19. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/pkg_update_check.py +82 -8
  20. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/vulcan/commands.py +4 -0
  21. {sima_cli-2.1.11 → sima_cli-2.1.12/sima_cli.egg-info}/PKG-INFO +1 -1
  22. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli.egg-info/SOURCES.txt +5 -0
  23. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_auth.py +9 -3
  24. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_auth0.py +30 -0
  25. sima_cli-2.1.12/tests/unit/test_generate_cli_markdown_docs.py +90 -0
  26. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_metadata_installer.py +159 -0
  27. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_metadata_validator.py +38 -1
  28. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_package_builder.py +9 -2
  29. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_pkg_update_check.py +63 -7
  30. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_sdk_image_detection.py +256 -11
  31. sima_cli-2.1.12/tests/unit/test_sdk_linux_shared_network.py +402 -0
  32. sima_cli-2.1.12/tests/unit/test_sdk_network_doctor.py +282 -0
  33. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_sdk_preinstall.py +73 -3
  34. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_vulcan.py +114 -0
  35. sima_cli-2.1.11/sima_cli/sdk/linux_shared_network.py +0 -579
  36. {sima_cli-2.1.11 → sima_cli-2.1.12}/LICENSE +0 -0
  37. {sima_cli-2.1.11 → sima_cli-2.1.12}/MANIFEST.in +0 -0
  38. {sima_cli-2.1.11 → sima_cli-2.1.12}/README.md +0 -0
  39. {sima_cli-2.1.11 → sima_cli-2.1.12}/requirements.txt +0 -0
  40. {sima_cli-2.1.11 → sima_cli-2.1.12}/setup.cfg +0 -0
  41. {sima_cli-2.1.11 → sima_cli-2.1.12}/setup.py +0 -0
  42. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/__init__.py +0 -0
  43. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/__main__.py +0 -0
  44. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/app_zoo/__init__.py +0 -0
  45. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/app_zoo/app.py +0 -0
  46. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/app_zoo/commands.py +0 -0
  47. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/auth/__init__.py +0 -0
  48. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/auth/login.py +0 -0
  49. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/data/__init__.py +0 -0
  50. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/data/resources_internal.yaml +0 -0
  51. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/data/resources_public.yaml +0 -0
  52. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/deploy_only/__init__.py +0 -0
  53. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/deploy_only/device/__init__.py +0 -0
  54. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/deploy_only/device/commands.py +0 -0
  55. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/deploy_only/mpk/__init__.py +0 -0
  56. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/deploy_only/mpk/commands.py +0 -0
  57. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/discover/__init__.py +0 -0
  58. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/discover/discover.py +0 -0
  59. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/discover/linuxll.py +0 -0
  60. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/download/__init__.py +0 -0
  61. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/download/downloader.py +0 -0
  62. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/__init__.py +0 -0
  63. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/github_assets.py +0 -0
  64. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/hostdriver.py +0 -0
  65. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/metadata_info.py +0 -0
  66. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/optiview.py +0 -0
  67. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/install/palette.py +0 -0
  68. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/mla/__init__.py +0 -0
  69. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/mla/meminfo.py +0 -0
  70. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/model_zoo/__init__.py +0 -0
  71. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/model_zoo/model.py +0 -0
  72. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/network/__init__.py +0 -0
  73. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/network/network.py +0 -0
  74. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/playbooks/__init__.py +0 -0
  75. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/playbooks/commands.py +0 -0
  76. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/playbooks/manager.py +0 -0
  77. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/__init__.py +0 -0
  78. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/cmdexec.py +0 -0
  79. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/config.py +0 -0
  80. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/neat.py +0 -0
  81. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/requirements.json +0 -0
  82. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/script.py +0 -0
  83. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/stop.py +0 -0
  84. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/sdk/uninstall.py +0 -0
  85. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/serial/__init__.py +0 -0
  86. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/serial/serial.py +0 -0
  87. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/storage/__init__.py +0 -0
  88. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/storage/nvme.py +0 -0
  89. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/storage/sdcard.py +0 -0
  90. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/__init__.py +0 -0
  91. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/bmaptool.py +0 -0
  92. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/bootimg.py +0 -0
  93. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/cleanlog.py +0 -0
  94. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/elxr.py +0 -0
  95. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/local.py +0 -0
  96. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/netboot.py +0 -0
  97. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/query.py +0 -0
  98. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/remote.py +0 -0
  99. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/update/updater.py +0 -0
  100. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/upgrade/__init__.py +0 -0
  101. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/upgrade/selfupdate.py +0 -0
  102. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/__init__.py +0 -0
  103. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/api_common.py +0 -0
  104. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/artifactory.py +0 -0
  105. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/common.py +0 -0
  106. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/config.py +0 -0
  107. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/config_loader.py +0 -0
  108. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/container_registries.py +0 -0
  109. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/deprecation.py +0 -0
  110. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/device_api.py +0 -0
  111. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/disk.py +0 -0
  112. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/docker.py +0 -0
  113. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/env.py +0 -0
  114. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/errors.py +0 -0
  115. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/mpk_api.py +0 -0
  116. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/net.py +0 -0
  117. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/network.py +0 -0
  118. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/pcie.py +0 -0
  119. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/serializers.py +0 -0
  120. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/services.py +0 -0
  121. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/utils/tag.py +0 -0
  122. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/vulcan/__init__.py +0 -0
  123. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli/vulcan/artifacts.py +0 -0
  124. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli.egg-info/dependency_links.txt +0 -0
  125. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli.egg-info/entry_points.txt +0 -0
  126. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli.egg-info/requires.txt +0 -0
  127. {sima_cli-2.1.11 → sima_cli-2.1.12}/sima_cli.egg-info/top_level.txt +0 -0
  128. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/__init__.py +0 -0
  129. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/e2e/__init__.py +0 -0
  130. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/__init__.py +0 -0
  131. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_app_zoo.py +0 -0
  132. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_appzoo_commands.py +0 -0
  133. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_cli.py +0 -0
  134. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_cli_stdio.py +0 -0
  135. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_cli_update.py +0 -0
  136. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_cli_update_rerun.py +0 -0
  137. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_deprecation.py +0 -0
  138. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_docker_utils.py +0 -0
  139. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_download.py +0 -0
  140. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_elxr_update.py +0 -0
  141. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_firmware.py +0 -0
  142. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_install_stub.py +0 -0
  143. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_model_zoo.py +0 -0
  144. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_netboot.py +0 -0
  145. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_sdk_uninstall.py +0 -0
  146. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_selfupdate.py +0 -0
  147. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_skills_commands.py +0 -0
  148. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_skills_manager.py +0 -0
  149. {sima_cli-2.1.11 → sima_cli-2.1.12}/tests/unit/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 2.1.11
3
+ Version: 2.1.12
4
4
  Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
5
5
  Home-page: https://community.sima.ai/
6
6
  Author: SiMa.ai
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sima-cli"
7
- version = "2.1.11"
7
+ version = "2.1.12"
8
8
  description = "CLI tool for SiMa Developer Portal to download models, firmware, and apps."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "2.1.11"
2
+ __version__ = "2.1.12"
@@ -286,7 +286,7 @@ def request_device_code(auth_cfg):
286
286
 
287
287
  def poll_for_token(auth_cfg, device_code, interval):
288
288
  """Step 2: Poll for user authorization."""
289
- print(f"⏳ Waiting for user authorization... polling every {interval} seconds.")
289
+ print(f"⏳ Waiting for authorization. Complete the browser login above; checking every {interval} seconds.")
290
290
  while True:
291
291
  time.sleep(interval)
292
292
  resp = requests.post(
@@ -333,16 +333,21 @@ def login_auth0(auth_cfg):
333
333
  highlighted_url = click.style(verify_complete, fg="cyan", bold=True)
334
334
  highlighted_code = click.style(user_code, fg="yellow", bold=True)
335
335
 
336
- print(f" Link expires in {expires_in} minutes.\n")
336
+ print("🔐 sima-cli needs you to authorize this device login.")
337
+ print(f"⏰ Login link/code expires in {expires_in} minutes.\n")
337
338
 
338
339
  # Auto-open browser if possible
339
- if is_browser_available() and verify_complete:
340
+ if is_browser_available() and verify_complete and webbrowser.open(verify_complete):
340
341
  print(f"🌐 Opening browser for login → {highlighted_url}")
341
- webbrowser.open(verify_complete)
342
+ print("After signing in, return to this terminal. sima-cli will continue automatically.")
342
343
  else:
343
- print("🔐 Browser not available open manually:")
344
- print(f" 👉 {highlighted_url}")
345
- print(f" 🪄 Code: {highlighted_code}")
344
+ print("⚠️ A browser could not be opened from this environment.")
345
+ print("Action required:")
346
+ print(" 1. Open this URL in a browser on your workstation:")
347
+ print(f" {highlighted_url}")
348
+ print(" 2. Sign in with your SiMa Developer Portal account.")
349
+ print(f" 3. If prompted, confirm this code: {highlighted_code}")
350
+ print(" 4. Return to this terminal. sima-cli will continue automatically after authorization.")
346
351
 
347
352
  return poll_for_token(auth_cfg, data["device_code"], data["interval"])
348
353
 
@@ -69,8 +69,17 @@ HEADERS = {
69
69
 
70
70
 
71
71
  def _prompt_manual_developer_portal_login(confirm_completion: bool = False) -> bool:
72
+ next_step = (
73
+ "After signing in and accepting the EULA, return here and confirm when prompted."
74
+ if confirm_completion
75
+ else "After signing in and accepting the EULA, rerun the command that required authentication."
76
+ )
72
77
  click.secho(
73
- f"\nOpen this page to accept EULA to proceed, press Y when you are done:\n{DEV_PORTAL_LOGIN_URL}\n",
78
+ "\nA browser could not be opened from this environment.\n"
79
+ "Open the following URL in a browser on your workstation to authenticate "
80
+ "with the SiMa Developer Portal and accept the EULA if prompted:\n\n"
81
+ f"{DEV_PORTAL_LOGIN_URL}\n\n"
82
+ f"{next_step}\n",
74
83
  fg="green",
75
84
  )
76
85
  if confirm_completion:
@@ -579,7 +579,7 @@ ALL_COMPONENTS = SDK_DEPENDENT_COMPONENTS | SDK_INDEPENDENT_COMPONENTS
579
579
  default=".",
580
580
  show_default=True,
581
581
  type=click.Path(file_okay=False, dir_okay=True, path_type=str),
582
- help="Directory where Neat package resources are downloaded and installed. Used with --neat or --vulcan.",
582
+ help="Directory where package resources are downloaded and installed.",
583
583
  )
584
584
  @click.option("--json", "json_output", is_flag=True, help="With --neat or --vulcan, print resolved metadata URL and exit.")
585
585
  @click.option(
@@ -655,6 +655,7 @@ def install_cmd(
655
655
  install_dir=install_dir,
656
656
  force=force,
657
657
  json_output=json_output,
658
+ command_name="sima-cli install",
658
659
  )
659
660
  return None
660
661
 
@@ -663,7 +664,13 @@ def install_cmd(
663
664
  if component:
664
665
  click.echo(f"⚠️ Component '{component}' is ignored when using --metadata. Proceeding with metadata-based installation.")
665
666
  click.echo(f"🔧 Installing generic component from metadata URL: {mirror}")
666
- install_from_metadata(metadata_url=mirror, internal=internal, force=force)
667
+ install_from_metadata(
668
+ metadata_url=mirror,
669
+ internal=internal,
670
+ install_dir=install_dir,
671
+ force=force,
672
+ command_name="sima-cli install",
673
+ )
667
674
  return None
668
675
 
669
676
  # No component and no metadata: error
@@ -673,7 +680,13 @@ def install_cmd(
673
680
 
674
681
  # if user specified gh: as component, treat it the same as -m
675
682
  if component.startswith("gh:"):
676
- install_from_metadata(metadata_url=component, internal=False, force=force)
683
+ install_from_metadata(
684
+ metadata_url=component,
685
+ internal=False,
686
+ install_dir=install_dir,
687
+ force=force,
688
+ command_name="sima-cli install",
689
+ )
677
690
  return None
678
691
 
679
692
  # if the user specified cr: or ghcr: as component, install from container registry
@@ -707,8 +720,16 @@ def install_cmd(
707
720
  try:
708
721
  metadata_url = metadata_resolver(component, version, tag)
709
722
  click.echo(f"🔧 Installing '{component}' from resolved metadata: {metadata_url}")
710
- if install_from_metadata(metadata_url=metadata_url, internal=internal, force=force):
723
+ if install_from_metadata(
724
+ metadata_url=metadata_url,
725
+ internal=internal,
726
+ install_dir=install_dir,
727
+ force=force,
728
+ command_name="sima-cli install",
729
+ ):
711
730
  click.echo("✅ Installation complete.")
731
+ except click.ClickException:
732
+ raise
712
733
  except Exception as e:
713
734
  click.echo(f"❌ Failed to resolve metadata for component '{component}': {e}")
714
735
  ctx.exit(1)
@@ -1,3 +1,4 @@
1
+ import platform
1
2
  import re
2
3
  from typing import Dict, List, Optional, Sequence
3
4
 
@@ -51,6 +52,33 @@ def validate_version_spec(spec: str) -> None:
51
52
  normalize_version_spec(spec)
52
53
 
53
54
 
55
+ def normalize_host_arch(arch: str) -> str:
56
+ if not isinstance(arch, str) or not arch.strip():
57
+ raise ValueError("host architecture must be a non-empty string")
58
+ normalized = arch.strip().lower()
59
+ aliases = {
60
+ "x86_64": "amd64",
61
+ "amd64": "amd64",
62
+ "aarch64": "arm64",
63
+ "arm64": "arm64",
64
+ }
65
+ if normalized not in aliases:
66
+ raise ValueError(
67
+ "invalid host architecture '{}'; supported values are amd64, arm64, x86_64, aarch64".format(
68
+ arch
69
+ )
70
+ )
71
+ return aliases[normalized]
72
+
73
+
74
+ def current_host_arch() -> str:
75
+ machine = platform.machine().lower()
76
+ try:
77
+ return normalize_host_arch(machine)
78
+ except ValueError:
79
+ return machine
80
+
81
+
54
82
  def normalize_exact_version(version: str) -> str:
55
83
  if not isinstance(version, str) or not version.strip():
56
84
  raise ValueError("version must be a non-empty string")
@@ -92,25 +120,48 @@ def version_matches(version: str, spec: str) -> bool:
92
120
  return True
93
121
 
94
122
 
95
- def parse_host_platform_specs(host_platforms: Optional[Sequence[str]]) -> List[Dict]:
123
+ def parse_host_platform_specs(
124
+ host_platforms: Optional[Sequence[str]],
125
+ host_arches: Optional[Sequence[str]] = None,
126
+ ) -> List[Dict]:
127
+ normalized_arches = []
128
+ seen_arches = set()
129
+ for raw_arch in host_arches or []:
130
+ for value in _split_csv(raw_arch):
131
+ arch = normalize_host_arch(value)
132
+ if arch not in seen_arches:
133
+ seen_arches.add(arch)
134
+ normalized_arches.append(arch)
135
+
96
136
  platforms = []
97
137
  for raw_spec in host_platforms or []:
98
138
  os_values = []
139
+ versions = {}
99
140
  seen = set()
100
141
  for value in _split_csv(raw_spec):
101
- os_value = value.lower()
142
+ raw_os, separator, raw_version_spec = value.partition("@")
143
+ os_value = raw_os.lower()
102
144
  if os_value not in VALID_OS:
103
145
  raise ValueError(
104
146
  "invalid host platform OS '{}'; supported values are {}".format(
105
- value, ", ".join(sorted(VALID_OS))
147
+ raw_os, ", ".join(sorted(VALID_OS))
106
148
  )
107
149
  )
108
150
  if os_value not in seen:
109
151
  seen.add(os_value)
110
152
  os_values.append(os_value)
153
+ if separator:
154
+ versions.setdefault(os_value, []).append(normalize_version_spec(raw_version_spec))
111
155
  if not os_values:
112
156
  raise ValueError("host platform spec must include at least one OS")
113
- platforms.append({"type": "host", "os": os_values})
157
+ platform_spec = {"type": "host", "os": os_values}
158
+ if versions:
159
+ platform_spec["versions"] = versions
160
+ if normalized_arches:
161
+ platform_spec["arch"] = list(normalized_arches)
162
+ platforms.append(platform_spec)
163
+ if normalized_arches and not platforms:
164
+ raise ValueError("--host-arch requires at least one --host-platform entry")
114
165
  return platforms
115
166
 
116
167
 
@@ -147,11 +198,12 @@ def parse_board_platform_specs(board_platforms: Optional[Sequence[str]]) -> List
147
198
 
148
199
  def build_platform_specs(
149
200
  host_platforms: Optional[Sequence[str]] = None,
201
+ host_arches: Optional[Sequence[str]] = None,
150
202
  board_platforms: Optional[Sequence[str]] = None,
151
203
  palette_platform: Optional[str] = None,
152
204
  ) -> List[Dict]:
153
205
  platforms = []
154
- platforms.extend(parse_host_platform_specs(host_platforms))
206
+ platforms.extend(parse_host_platform_specs(host_platforms, host_arches=host_arches))
155
207
  platforms.extend(parse_board_platform_specs(board_platforms))
156
208
  if palette_platform is not None:
157
209
  platform = {"type": "palette"}
@@ -20,6 +20,7 @@ import requests
20
20
 
21
21
  from rich.console import Console
22
22
  from rich.panel import Panel
23
+ from rich.text import Text
23
24
  from typing import Tuple
24
25
 
25
26
  from huggingface_hub import snapshot_download
@@ -28,7 +29,7 @@ from sima_cli.utils.disk import check_disk_space
28
29
  from sima_cli.utils.env import get_environment_type, get_exact_devkit_type, get_sima_build_version
29
30
  from sima_cli.download.downloader import download_file_from_url
30
31
  from sima_cli.install.metadata_validator import validate_metadata, MetadataValidationError
31
- from sima_cli.install.compatibility import version_matches
32
+ from sima_cli.install.compatibility import current_host_arch, normalize_host_arch, version_matches
32
33
  from sima_cli.install.metadata_info import print_metadata_summary, parse_size_string_to_bytes
33
34
  from sima_cli.utils.container_registries import install_from_cr
34
35
  from sima_cli.install.registry import PackageRegistry
@@ -36,6 +37,52 @@ from sima_cli.install.registry import PackageRegistry
36
37
  console = Console()
37
38
  registry = PackageRegistry()
38
39
 
40
+ class InstallationPreflightError(click.ClickException):
41
+ def show(self, file=None) -> None:
42
+ console.print(
43
+ Panel(
44
+ Text(str(self.message), style="yellow"),
45
+ title="Installation Failed",
46
+ border_style="yellow",
47
+ expand=False,
48
+ )
49
+ )
50
+
51
+
52
+ def _ensure_install_dir_writable(install_dir: str, command_name: str = "sima-cli install") -> None:
53
+ target = Path(install_dir or ".").expanduser()
54
+ display_path = Path.cwd() if str(target) == "." else target
55
+
56
+ check_dir = target
57
+ if not check_dir.exists():
58
+ check_dir = target.parent
59
+ while not check_dir.exists() and check_dir != check_dir.parent:
60
+ check_dir = check_dir.parent
61
+
62
+ if not check_dir.is_dir():
63
+ raise click.ClickException(f"Install path '{display_path}' is not a directory.")
64
+
65
+ try:
66
+ with tempfile.NamedTemporaryFile(prefix=".sima-cli-write-test-", dir=str(check_dir)):
67
+ pass
68
+ except OSError as exc:
69
+ if str(target) == ".":
70
+ raise InstallationPreflightError(
71
+ f"Current directory '{Path.cwd()}' is not writable.\n\n"
72
+ "This install downloads package assets into the current directory before installation.\n\n"
73
+ "Run the command again from a writable work directory, for example:\n"
74
+ " mkdir -p ~/sima-install && cd ~/sima-install\n"
75
+ f" {command_name} ...\n\n"
76
+ "Or choose a destination explicitly:\n"
77
+ f" {command_name} ... --install-dir <writable-directory>"
78
+ ) from exc
79
+ raise InstallationPreflightError(
80
+ f"Install directory '{display_path}' is not writable.\n\n"
81
+ "This install downloads package assets into the install directory before installation.\n\n"
82
+ "Choose a writable destination and rerun the command:\n"
83
+ f" {command_name} ... --install-dir <writable-directory>"
84
+ ) from exc
85
+
39
86
  def _copy_dir(src: Path, dest: Path, label: str):
40
87
  """
41
88
  Copy files from src → dest, merging with existing files (no deletion).
@@ -1065,23 +1112,26 @@ def _print_compatible_platforms(platforms):
1065
1112
  table.add_column("Platform Type", style="bold cyan", no_wrap=True)
1066
1113
  table.add_column("Details", style="bold yellow")
1067
1114
  table.add_column("Supported Versions / Targets", style="white")
1115
+ table.add_column("Arch", style="white")
1068
1116
 
1069
1117
  for p in platforms:
1070
1118
  ptype = p.get("type", "N/A")
1071
1119
 
1072
1120
  if ptype == "host":
1121
+ arch = ", ".join(p.get("arch") or ["All"])
1122
+ versions_by_os = {str(k).lower(): v for k, v in p.get("versions", {}).items()}
1073
1123
  for os_name in p.get("os", []):
1074
- versions = p.get("versions", {}).get(os_name, ["All"])
1075
- table.add_row(ptype, os_name.capitalize(), ", ".join(versions))
1124
+ versions = versions_by_os.get(str(os_name).lower(), ["All"])
1125
+ table.add_row(ptype, os_name.capitalize(), ", ".join(versions), arch)
1076
1126
 
1077
1127
  elif ptype == "board":
1078
1128
  compat = p.get("compatible_with", [])
1079
1129
  version = p.get("version", "All")
1080
1130
  compat_text = ", ".join(compat) if compat else "N/A"
1081
- table.add_row(ptype, compat_text, version)
1131
+ table.add_row(ptype, compat_text, version, "N/A")
1082
1132
 
1083
1133
  else:
1084
- table.add_row(ptype, "N/A", "N/A")
1134
+ table.add_row(ptype, "N/A", "N/A", "N/A")
1085
1135
 
1086
1136
  console.print(table)
1087
1137
  console.print()
@@ -1091,8 +1141,14 @@ def _compare_versions(current: str, condition: str) -> bool:
1091
1141
  Compare current version (e.g. '15.5') against a condition string like:
1092
1142
  '>=12', '<=16', '>20.04', '<23.0', '14'
1093
1143
  """
1094
- cur = _version_to_tuple(current)
1095
1144
  cond = condition.strip()
1145
+ if re.match(r"^(>=|<=|==|=|>|<)", cond):
1146
+ try:
1147
+ return version_matches(current, cond)
1148
+ except ValueError:
1149
+ pass
1150
+
1151
+ cur = _version_to_tuple(current)
1096
1152
 
1097
1153
  # Detect operator and target
1098
1154
  match = re.match(r"^(>=|<=|>|<|=)?\s*([\d.]+)$", cond)
@@ -1128,6 +1184,73 @@ def _get_palette_sdk_version(release_file: Path = Path("/etc/sdk-release")) -> s
1128
1184
  return match.group(1).split("_", 1)[0].strip()
1129
1185
 
1130
1186
 
1187
+ def _detected_host_platform() -> tuple:
1188
+ os_name = platform.system().lower()
1189
+ os_version = "Unknown"
1190
+
1191
+ if os_name == "darwin":
1192
+ return "mac", platform.mac_ver()[0] or "Unknown", current_host_arch()
1193
+ if os_name == "windows":
1194
+ return "windows", platform.release() or "Unknown", current_host_arch()
1195
+ if os_name != "linux":
1196
+ return os_name, os_version, current_host_arch()
1197
+
1198
+ os_release = {}
1199
+ try:
1200
+ with open("/etc/os-release", encoding="utf-8") as f:
1201
+ for line in f:
1202
+ key, separator, value = line.strip().partition("=")
1203
+ if separator:
1204
+ os_release[key] = value.strip().strip('"')
1205
+ except OSError:
1206
+ os_release = {}
1207
+
1208
+ distro_id = (os_release.get("ID") or "").lower()
1209
+ version_id = os_release.get("VERSION_ID") or ""
1210
+ if distro_id == "ubuntu":
1211
+ os_name = "ubuntu"
1212
+ os_version = version_id or "Unknown"
1213
+ else:
1214
+ os_name = "linux"
1215
+ os_version = version_id or "Unknown"
1216
+
1217
+ if os_version == "Unknown":
1218
+ try:
1219
+ out = subprocess.check_output(["lsb_release", "-ds"], text=True).strip().lower()
1220
+ if "ubuntu" in out:
1221
+ os_name = "ubuntu"
1222
+ match = re.search(r"(\d+\.\d+|\d+)", out)
1223
+ os_version = match.group(1) if match else "Unknown"
1224
+ except Exception:
1225
+ pass
1226
+
1227
+ match = re.search(r"(\d+\.\d+|\d+)", os_version)
1228
+ if match:
1229
+ os_version = match.group(1)
1230
+ return os_name, os_version, current_host_arch()
1231
+
1232
+
1233
+ def _host_os_matches(detected_os: str, supported_oses: List[str]) -> bool:
1234
+ if detected_os in supported_oses:
1235
+ return True
1236
+ return detected_os == "ubuntu" and "linux" in supported_oses
1237
+
1238
+
1239
+ def _host_versions_for_os(platform_entry: dict, detected_os: str) -> list:
1240
+ versions_dict = {str(k).lower(): v for k, v in platform_entry.get("versions", {}).items()}
1241
+ return versions_dict.get(detected_os) or (versions_dict.get("linux") if detected_os == "ubuntu" else []) or []
1242
+
1243
+
1244
+ def _host_arch_matches(platform_entry: dict, detected_arch: str) -> bool:
1245
+ supported_arches = []
1246
+ for value in platform_entry.get("arch", []):
1247
+ try:
1248
+ supported_arches.append(normalize_host_arch(value))
1249
+ except ValueError:
1250
+ supported_arches.append(str(value).lower())
1251
+ return not supported_arches or detected_arch in supported_arches
1252
+
1253
+
1131
1254
  def _is_platform_compatible(metadata: dict, force: bool = False) -> bool:
1132
1255
  """
1133
1256
  Determines if the current environment is compatible with the package metadata.
@@ -1142,40 +1265,7 @@ def _is_platform_compatible(metadata: dict, force: bool = False) -> bool:
1142
1265
  click.echo("ℹ️ No platform restrictions specified; treating package as compatible with all platforms.")
1143
1266
  return True
1144
1267
 
1145
- # Detect current OS and version
1146
- os_name = platform.system().lower() # 'darwin', 'windows', 'linux'
1147
- os_version = "Unknown"
1148
-
1149
- if os_name == "darwin":
1150
- os_name = "mac"
1151
- os_version = platform.mac_ver()[0] or "Unknown"
1152
- elif os_name == "windows":
1153
- os_version = platform.release()
1154
- elif os_name == "linux":
1155
- try:
1156
- out = subprocess.check_output(["lsb_release", "-ds"], text=True).strip().lower()
1157
- if "ubuntu" in out:
1158
- os_name = "ubuntu"
1159
- match = re.search(r"(\d+\.\d+)", out)
1160
- os_version = match.group(1) if match else "Unknown"
1161
- else:
1162
- os_name = "linux"
1163
- match = re.search(r"(\d+\.\d+)", out)
1164
- os_version = match.group(1) if match else "Unknown"
1165
- except Exception:
1166
- os_name = "linux"
1167
- try:
1168
- with open("/etc/os-release") as f:
1169
- for line in f:
1170
- if line.startswith("VERSION_ID"):
1171
- os_version = line.split("=")[1].strip().strip('"')
1172
- break
1173
- except FileNotFoundError:
1174
- os_version = "Unknown"
1175
-
1176
- match = re.search(r"(\d+\.\d+|\d+)", os_version)
1177
- if match:
1178
- os_version = match.group(1)
1268
+ os_name, os_version, host_arch = _detected_host_platform()
1179
1269
 
1180
1270
  # ──────────────────────────────────────────────
1181
1271
  # Compatibility checks
@@ -1214,16 +1304,19 @@ def _is_platform_compatible(metadata: dict, force: bool = False) -> bool:
1214
1304
 
1215
1305
  # 2️⃣ OS match (mac, ubuntu, linux, windows)
1216
1306
  supported_oses = [o.lower() for o in platform_entry.get("os", [])]
1217
- if os_name not in supported_oses:
1218
- if not (os_name == "ubuntu" and "linux" in supported_oses):
1219
- continue
1307
+ if not _host_os_matches(os_name, supported_oses):
1308
+ continue
1220
1309
 
1221
- # 3️⃣ Version match
1222
- versions_dict = platform_entry.get("versions", {})
1223
- supported_versions = (
1224
- versions_dict.get(os_name)
1225
- or (versions_dict.get("linux") if os_name == "ubuntu" else [])
1226
- )
1310
+ # 3️⃣ Architecture match
1311
+ if not _host_arch_matches(platform_entry, host_arch):
1312
+ click.echo(
1313
+ f"❌ Host architecture {host_arch or 'unknown'} is not supported. "
1314
+ f"Allowed: {platform_entry.get('arch')}"
1315
+ )
1316
+ continue
1317
+
1318
+ # 4️⃣ Version match
1319
+ supported_versions = _host_versions_for_os(platform_entry, os_name)
1227
1320
 
1228
1321
  if supported_versions:
1229
1322
  ok = False
@@ -1248,11 +1341,12 @@ def _is_platform_compatible(metadata: dict, force: bool = False) -> bool:
1248
1341
 
1249
1342
  click.secho(
1250
1343
  f"❌ Current environment [{env_type}:{env_subtype}] "
1251
- f"({os_name} {os_version}) is not compatible with this package.", fg='red'
1344
+ f"({os_name} {os_version} {host_arch}) is not compatible with this package.", fg='red'
1252
1345
  )
1253
1346
  _print_compatible_platforms(platforms)
1254
1347
  if not force:
1255
1348
  exit(1)
1349
+ return False
1256
1350
 
1257
1351
 
1258
1352
  def _print_post_install_message(metadata: Dict):
@@ -1413,7 +1507,15 @@ def _resolve_github_metadata_url(gh_ref: str) -> Tuple[str, str]:
1413
1507
  except Exception as e:
1414
1508
  raise RuntimeError(f"Failed to resolve GitHub metadata URL {gh_ref}: {e}")
1415
1509
 
1416
- def install_from_metadata(metadata_url: str, internal: bool, install_dir: str = '.', force: bool = False):
1510
+ def install_from_metadata(
1511
+ metadata_url: str,
1512
+ internal: bool,
1513
+ install_dir: str = '.',
1514
+ force: bool = False,
1515
+ command_name: str = "sima-cli install",
1516
+ ):
1517
+ _ensure_install_dir_writable(install_dir, command_name=command_name)
1518
+
1417
1519
  try:
1418
1520
  tag = None
1419
1521
 
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from sima_cli.install.compatibility import (
7
7
  VALID_OS,
8
8
  VALID_PLATFORM_TYPES,
9
+ normalize_host_arch,
9
10
  normalize_exact_version,
10
11
  validate_version_spec,
11
12
  )
@@ -85,6 +86,45 @@ def validate_metadata(data: dict):
85
86
  f"Invalid OS '{os_value}' in platform entry {i}. Supported: {VALID_OS}"
86
87
  )
87
88
 
89
+ if "versions" in platform:
90
+ versions = platform["versions"]
91
+ if not isinstance(versions, dict):
92
+ raise MetadataValidationError(f"'versions' must be an object for host in entry {i}")
93
+ for os_key, specs in versions.items():
94
+ if not isinstance(os_key, str) or os_key.lower() not in VALID_OS:
95
+ raise MetadataValidationError(
96
+ f"Invalid OS '{os_key}' in host versions for platform entry {i}. Supported: {VALID_OS}"
97
+ )
98
+ if os_key.lower() not in [str(value).lower() for value in platform["os"]]:
99
+ raise MetadataValidationError(
100
+ f"Host versions key '{os_key}' must also be listed in 'os' for platform entry {i}"
101
+ )
102
+ if not isinstance(specs, list) or not specs:
103
+ raise MetadataValidationError(
104
+ f"'versions.{os_key}' must be a non-empty list in platform entry {i}"
105
+ )
106
+ for spec in specs:
107
+ if not isinstance(spec, str):
108
+ raise MetadataValidationError(
109
+ f"'versions.{os_key}' values must be strings in platform entry {i}"
110
+ )
111
+ try:
112
+ validate_version_spec(spec)
113
+ except ValueError as exc:
114
+ raise MetadataValidationError(
115
+ f"Invalid host version spec for '{os_key}' in entry {i}: {exc}"
116
+ )
117
+
118
+ if "arch" in platform:
119
+ arches = platform["arch"]
120
+ if not isinstance(arches, list) or not arches:
121
+ raise MetadataValidationError(f"'arch' must be a non-empty list for host in entry {i}")
122
+ for arch in arches:
123
+ try:
124
+ normalize_host_arch(arch)
125
+ except ValueError as exc:
126
+ raise MetadataValidationError(f"Invalid host architecture in entry {i}: {exc}")
127
+
88
128
  if platform["type"] == "palette" and "version" in platform:
89
129
  if not isinstance(platform["version"], str):
90
130
  raise MetadataValidationError(f"'version' must be a string for palette in entry {i}")
@@ -246,6 +246,7 @@ def build_metadata(
246
246
  exclude: Optional[Sequence[str]] = None,
247
247
  download_compatible_files_only: bool = False,
248
248
  host_platforms: Optional[Sequence[str]] = None,
249
+ host_arches: Optional[Sequence[str]] = None,
249
250
  board_platforms: Optional[Sequence[str]] = None,
250
251
  palette_platform: Optional[str] = None,
251
252
  ) -> Dict:
@@ -279,6 +280,7 @@ def build_metadata(
279
280
  "description": resolved_description or "",
280
281
  "platforms": build_platform_specs(
281
282
  host_platforms=host_platforms,
283
+ host_arches=host_arches,
282
284
  board_platforms=board_platforms,
283
285
  palette_platform=palette_platform,
284
286
  ),
@@ -135,10 +135,19 @@ def show_metadata(name, version):
135
135
  "--host-platform",
136
136
  multiple=True,
137
137
  help=(
138
- "Host OS compatibility as a comma-separated list. Supported values: "
138
+ "Host OS compatibility as OS[,OS...][@VERSION_SPEC], for example "
139
+ "linux, ubuntu@==24.04, or ubuntu@>=22.04,<=24.04. Supported OS values: "
139
140
  "linux, ubuntu, mac, windows. May be repeated."
140
141
  ),
141
142
  )
143
+ @click.option(
144
+ "--host-arch",
145
+ multiple=True,
146
+ help=(
147
+ "Host CPU architecture compatibility as ARCH[,ARCH...], for example amd64 or arm64. "
148
+ "Aliases x86_64 and aarch64 are normalized. Requires --host-platform."
149
+ ),
150
+ )
142
151
  @click.option(
143
152
  "--board-platform",
144
153
  multiple=True,
@@ -168,6 +177,7 @@ def build_package_metadata(
168
177
  variant,
169
178
  download_compatible_files_only,
170
179
  host_platform,
180
+ host_arch,
171
181
  board_platform,
172
182
  palette_platform,
173
183
  ):
@@ -185,6 +195,7 @@ def build_package_metadata(
185
195
  exclude=exclude,
186
196
  download_compatible_files_only=download_compatible_files_only,
187
197
  host_platforms=host_platform,
198
+ host_arches=host_arch,
188
199
  board_platforms=board_platform,
189
200
  palette_platform=palette_platform,
190
201
  )