sima-cli 0.0.36__py3-none-any.whl → 0.0.38__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 +1 -1
- sima_cli/app_zoo/app.py +435 -0
- sima_cli/cli.py +70 -4
- sima_cli/install/metadata_installer.py +142 -33
- sima_cli/install/metadata_validator.py +11 -2
- sima_cli/model_zoo/model.py +76 -28
- sima_cli/update/netboot.py +37 -24
- sima_cli/update/updater.py +22 -3
- sima_cli/utils/env.py +59 -1
- sima_cli/utils/pkg_update_check.py +11 -8
- {sima_cli-0.0.36.dist-info → sima_cli-0.0.38.dist-info}/METADATA +1 -1
- {sima_cli-0.0.36.dist-info → sima_cli-0.0.38.dist-info}/RECORD +16 -16
- {sima_cli-0.0.36.dist-info → sima_cli-0.0.38.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.36.dist-info → sima_cli-0.0.38.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.36.dist-info → sima_cli-0.0.38.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.36.dist-info → sima_cli-0.0.38.dist-info}/top_level.txt +0 -0
@@ -8,11 +8,10 @@ import shutil
|
|
8
8
|
import tarfile
|
9
9
|
import zipfile
|
10
10
|
import stat
|
11
|
-
|
12
|
-
|
11
|
+
import shlex
|
12
|
+
from urllib.parse import urlparse, quote, urljoin
|
13
13
|
from typing import Dict
|
14
14
|
from tqdm import tqdm
|
15
|
-
from urllib.parse import urljoin
|
16
15
|
from pathlib import Path
|
17
16
|
import subprocess
|
18
17
|
import requests
|
@@ -23,7 +22,7 @@ from rich.panel import Panel
|
|
23
22
|
from huggingface_hub import snapshot_download
|
24
23
|
|
25
24
|
from sima_cli.utils.disk import check_disk_space
|
26
|
-
from sima_cli.utils.env import get_environment_type, get_exact_devkit_type
|
25
|
+
from sima_cli.utils.env import get_environment_type, get_exact_devkit_type, get_sima_build_version
|
27
26
|
from sima_cli.download.downloader import download_file_from_url
|
28
27
|
from sima_cli.install.metadata_validator import validate_metadata, MetadataValidationError
|
29
28
|
from sima_cli.install.metadata_info import print_metadata_summary, parse_size_string_to_bytes
|
@@ -33,22 +32,22 @@ console = Console()
|
|
33
32
|
def _copy_dir(src: Path, dest: Path, label: str):
|
34
33
|
"""
|
35
34
|
Copy files from src → dest, merging with existing files (no deletion).
|
35
|
+
Does NOT overwrite files if they already exist.
|
36
36
|
Ensures that all parent directories for dest are created.
|
37
37
|
"""
|
38
38
|
if not src.exists():
|
39
39
|
raise FileNotFoundError(f"SDK {label} not found: {src}")
|
40
40
|
|
41
|
-
# Ensure full path exists
|
42
41
|
dest.mkdir(parents=True, exist_ok=True)
|
43
42
|
|
44
|
-
# Copy tree correctly
|
45
43
|
for item in src.iterdir():
|
46
44
|
target = dest / item.name
|
47
45
|
if item.is_dir():
|
48
|
-
|
46
|
+
_copy_dir(item, target, label)
|
49
47
|
else:
|
50
|
-
|
51
|
-
|
48
|
+
if not target.exists():
|
49
|
+
shutil.copy2(item, target)
|
50
|
+
|
52
51
|
click.echo(f"✅ Copied {label} into {dest}")
|
53
52
|
|
54
53
|
def _prepare_pipeline_project(repo_dir: Path):
|
@@ -98,6 +97,8 @@ def _prepare_pipeline_project(repo_dir: Path):
|
|
98
97
|
|
99
98
|
# Step b/c: scan plugin paths and copy SDK plugins
|
100
99
|
sdk_plugins_base = Path("/usr/local/simaai/plugin_zoo/gst-simaai-plugins-base/gst")
|
100
|
+
sdk_alt_base = sdk_plugins_base / "PyGast-plugins"
|
101
|
+
|
101
102
|
dest_plugins_dir = repo_dir / "plugins"
|
102
103
|
dest_plugins_dir.mkdir(exist_ok=True)
|
103
104
|
|
@@ -111,27 +112,94 @@ def _prepare_pipeline_project(repo_dir: Path):
|
|
111
112
|
continue
|
112
113
|
|
113
114
|
plugin_name = parts[1]
|
115
|
+
|
116
|
+
# Look first in gst/, then fallback to gst/PyGast-plugins/
|
114
117
|
sdk_plugin_path = sdk_plugins_base / plugin_name
|
118
|
+
if not sdk_plugin_path.exists():
|
119
|
+
sdk_plugin_path = sdk_alt_base / plugin_name
|
120
|
+
|
115
121
|
if not sdk_plugin_path.exists():
|
116
122
|
click.echo(
|
117
|
-
f"⚠️
|
118
|
-
"It
|
123
|
+
f"⚠️ Missing plugin source: {plugin_name} in the SDK, skipping. "
|
124
|
+
"It is likely a custom plugin already in the repo so it's safe to ignore this warning."
|
119
125
|
)
|
120
126
|
continue
|
121
127
|
|
122
128
|
dest_plugin_path = dest_plugins_dir / plugin_name
|
123
129
|
dest_plugin_path.mkdir(parents=True, exist_ok=True)
|
124
130
|
|
125
|
-
#
|
126
|
-
|
131
|
+
# Walk the SDK plugin dir and copy only missing files
|
132
|
+
for src_file in sdk_plugin_path.rglob("*"):
|
133
|
+
if src_file.is_file():
|
134
|
+
rel_path = src_file.relative_to(sdk_plugin_path)
|
135
|
+
dest_file = dest_plugin_path / rel_path
|
136
|
+
if dest_file.exists():
|
137
|
+
click.echo(f"↩️ Skipped existing file in the repo: {dest_file}")
|
138
|
+
continue
|
139
|
+
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
140
|
+
shutil.copy2(src_file, dest_file)
|
127
141
|
|
128
|
-
click.echo(f"✅ Copied plugin {plugin_name} into {dest_plugin_path}")
|
142
|
+
click.echo(f"✅ Copied plugin {plugin_name} into {dest_plugin_path} (safe copy)")
|
129
143
|
|
130
144
|
except Exception as e:
|
131
145
|
click.echo(f"❌ Error copying plugin {plugin}: {e}")
|
132
146
|
|
133
147
|
click.echo("🎉 Pipeline project prepared.")
|
134
148
|
|
149
|
+
def _download_requirements_wheels(repo_dir: Path):
|
150
|
+
"""
|
151
|
+
Look for resources/dependencies/requirements.txt under the repo,
|
152
|
+
parse each line, and download wheels into the same folder.
|
153
|
+
Supports optional pip download flags in parentheses.
|
154
|
+
|
155
|
+
Example line formats:
|
156
|
+
jax==0.6.2
|
157
|
+
jaxlib==0.6.2 (--platform manylinux2014_aarch64 --python-version 310 --abi cp310)
|
158
|
+
"""
|
159
|
+
deps_dir = repo_dir / "resources" / "dependencies"
|
160
|
+
req_file = deps_dir / "requirements.txt"
|
161
|
+
|
162
|
+
if not req_file.exists():
|
163
|
+
click.echo("⚠️ No requirements.txt found under resources/dependencies in the repo, skipping wheel download, safe to ignore this message")
|
164
|
+
return
|
165
|
+
|
166
|
+
deps_dir.mkdir(parents=True, exist_ok=True)
|
167
|
+
|
168
|
+
with req_file.open("r") as f:
|
169
|
+
lines = [line.strip() for line in f if line.strip() and not line.startswith("#")]
|
170
|
+
|
171
|
+
if not lines:
|
172
|
+
click.echo("⚠️ requirements.txt is empty, nothing to download.")
|
173
|
+
return
|
174
|
+
|
175
|
+
for line in lines:
|
176
|
+
# Split package and extra params if present
|
177
|
+
if "(" in line and ")" in line:
|
178
|
+
pkg_part, extra = line.split("(", 1)
|
179
|
+
package = pkg_part.strip()
|
180
|
+
extra_args = shlex.split(extra.strip(") "))
|
181
|
+
else:
|
182
|
+
package = line.strip()
|
183
|
+
extra_args = []
|
184
|
+
|
185
|
+
click.echo(f"⬇️ Downloading {package} {extra_args if extra_args else ''}")
|
186
|
+
|
187
|
+
try:
|
188
|
+
cmd = [
|
189
|
+
"pip3", "download", "--no-deps",
|
190
|
+
"--only-binary=:all:",
|
191
|
+
"-d", str(deps_dir),
|
192
|
+
package,
|
193
|
+
] + extra_args
|
194
|
+
|
195
|
+
rc = os.system(" ".join(shlex.quote(c) for c in cmd))
|
196
|
+
if rc != 0:
|
197
|
+
click.echo(f"❌ pip download failed for {package}")
|
198
|
+
else:
|
199
|
+
click.echo(f"✅ Downloaded {package} into {deps_dir}")
|
200
|
+
except Exception as e:
|
201
|
+
click.echo(f"❌ Error downloading {package}: {e}")
|
202
|
+
|
135
203
|
def _download_github_repo(owner: str, repo: str, ref: str, dest_folder: str, token: str = None) -> str:
|
136
204
|
"""
|
137
205
|
Download and extract a GitHub repo tarball via the REST API (no git required).
|
@@ -146,7 +214,15 @@ def _download_github_repo(owner: str, repo: str, ref: str, dest_folder: str, tok
|
|
146
214
|
Returns:
|
147
215
|
str: Path to the extracted repo
|
148
216
|
"""
|
149
|
-
|
217
|
+
# Encode ref for API, but sanitize separately for filesystem usage
|
218
|
+
if ref:
|
219
|
+
ref_encoded = quote(ref, safe="") # safe for URL
|
220
|
+
ref_safe = ref.replace("/", "_") # safe for filesystem
|
221
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/tarball/{ref_encoded}"
|
222
|
+
else:
|
223
|
+
ref_encoded = ref_safe = None
|
224
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/tarball"
|
225
|
+
|
150
226
|
headers = {}
|
151
227
|
if token:
|
152
228
|
headers["Authorization"] = f"Bearer {token}"
|
@@ -163,12 +239,16 @@ def _download_github_repo(owner: str, repo: str, ref: str, dest_folder: str, tok
|
|
163
239
|
tmp_file.write(chunk)
|
164
240
|
tmp_path = Path(tmp_file.name)
|
165
241
|
|
166
|
-
|
242
|
+
# Use sanitized ref in folder name (if provided)
|
243
|
+
repo_dir_name = f"{repo}-{ref_safe}" if ref_safe else repo
|
244
|
+
repo_dir = Path(dest_folder) / repo_dir_name
|
167
245
|
repo_dir.mkdir(parents=True, exist_ok=True)
|
168
246
|
|
169
247
|
_extract_tar_strip_top_level(tmp_path, repo_dir)
|
170
248
|
tmp_path.unlink(missing_ok=True)
|
249
|
+
|
171
250
|
click.echo(f"✅ Downloaded GitHub repo to folder: {repo_dir}")
|
251
|
+
_download_requirements_wheels(repo_dir=repo_dir)
|
172
252
|
|
173
253
|
try:
|
174
254
|
_prepare_pipeline_project(repo_dir)
|
@@ -177,7 +257,7 @@ def _download_github_repo(owner: str, repo: str, ref: str, dest_folder: str, tok
|
|
177
257
|
|
178
258
|
return str(repo_dir)
|
179
259
|
|
180
|
-
def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal: bool = False, skip_models: bool = False) -> list:
|
260
|
+
def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal: bool = False, skip_models: bool = False, tag: str = None) -> list:
|
181
261
|
"""
|
182
262
|
Downloads resources defined in metadata to a local destination folder.
|
183
263
|
|
@@ -187,6 +267,7 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
187
267
|
dest_folder (str): Local path to download resources into
|
188
268
|
internal (bool): Whether to use internal download routing (if applicable)
|
189
269
|
skip_models (bool): If True, skips downloading any file path starting with 'models/'
|
270
|
+
tag (str): metadata.json tag from GitHub will be passed into the resources in the file
|
190
271
|
|
191
272
|
Returns:
|
192
273
|
list: Paths to the downloaded local files
|
@@ -228,9 +309,10 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
228
309
|
org, name = repo_id.split("/", 1)
|
229
310
|
target_dir = os.path.join(dest_folder, name)
|
230
311
|
|
231
|
-
click.echo(f"🤗 Downloading Hugging Face repo: {repo_id}" + (f"@{revision}" if revision else ""))
|
312
|
+
click.echo(f"🤗 Downloading Hugging Face repo: {org}/{repo_id}" + (f"@{revision}" if revision else ""))
|
232
313
|
model_path = snapshot_download(
|
233
314
|
repo_id=repo_id,
|
315
|
+
revision=revision,
|
234
316
|
local_dir=target_dir,
|
235
317
|
local_dir_use_symlinks=False
|
236
318
|
)
|
@@ -239,10 +321,11 @@ def _download_assets(metadata: dict, base_url: str, dest_folder: str, internal:
|
|
239
321
|
|
240
322
|
if resource.startswith("gh:"):
|
241
323
|
resource_spec = resource[3:]
|
324
|
+
|
242
325
|
if "@" in resource_spec:
|
243
326
|
repo_id, ref = resource_spec.split("@", 1)
|
244
327
|
else:
|
245
|
-
repo_id, ref = resource_spec,
|
328
|
+
repo_id, ref = resource_spec, tag
|
246
329
|
|
247
330
|
if "/" not in repo_id:
|
248
331
|
raise click.ClickException(f"❌ Invalid GitHub repo spec: {resource}")
|
@@ -668,13 +751,19 @@ def _run_installation_script(metadata: Dict, extract_path: str = "."):
|
|
668
751
|
|
669
752
|
print("✅ Installation completed successfully.")
|
670
753
|
|
671
|
-
def _resolve_github_metadata_url(gh_ref: str) -> str:
|
754
|
+
def _resolve_github_metadata_url(gh_ref: str) -> tuple[str, str]:
|
672
755
|
"""
|
673
|
-
Resolve a GitHub shorthand like gh:org/repo@tag into a
|
756
|
+
Resolve a GitHub shorthand like gh:org/repo@tag into a local metadata.json file path.
|
674
757
|
If tag is omitted, defaults to 'main'.
|
758
|
+
|
759
|
+
Args:
|
760
|
+
gh_ref (str): Reference in the form 'gh:org/repo@tag'
|
761
|
+
|
762
|
+
Returns:
|
763
|
+
tuple[str, str]: (local_path_to_metadata_json, tag_used)
|
675
764
|
"""
|
676
765
|
try:
|
677
|
-
_, repo_ref = gh_ref.split(":", 1) #
|
766
|
+
_, repo_ref = gh_ref.split(":", 1) # strip 'gh:'
|
678
767
|
if "@" in repo_ref:
|
679
768
|
org_repo, tag = repo_ref.split("@", 1)
|
680
769
|
else:
|
@@ -683,28 +772,38 @@ def _resolve_github_metadata_url(gh_ref: str) -> str:
|
|
683
772
|
owner, repo = org_repo.split("/", 1)
|
684
773
|
token = os.getenv("GITHUB_TOKEN")
|
685
774
|
|
686
|
-
#
|
687
|
-
|
775
|
+
# Encode the ref safely for GitHub API
|
776
|
+
tag_encoded = quote(tag, safe="")
|
777
|
+
|
778
|
+
# GitHub API URL for raw file contents
|
779
|
+
api_url = (
|
780
|
+
f"https://api.github.com/repos/{owner}/{repo}/contents/metadata.json?ref={tag_encoded}"
|
781
|
+
)
|
688
782
|
headers = {"Accept": "application/vnd.github.v3.raw"}
|
689
783
|
if token:
|
690
|
-
headers["Authorization"] = f"
|
784
|
+
headers["Authorization"] = f"Bearer {token}"
|
691
785
|
|
692
786
|
r = requests.get(api_url, headers=headers)
|
693
787
|
r.raise_for_status()
|
694
|
-
|
695
|
-
#
|
696
|
-
|
788
|
+
|
789
|
+
# --- Sanitize tag for filesystem use ---
|
790
|
+
tag_safe = tag.replace("/", "_")
|
791
|
+
|
792
|
+
# Write metadata.json locally
|
793
|
+
local_path = os.path.join("/tmp", f"{repo}-{tag_safe}-metadata.json")
|
697
794
|
with open(local_path, "wb") as f:
|
698
795
|
f.write(r.content)
|
699
796
|
|
700
|
-
return local_path
|
797
|
+
return local_path, tag
|
701
798
|
except Exception as e:
|
702
799
|
raise RuntimeError(f"Failed to resolve GitHub metadata URL {gh_ref}: {e}")
|
703
800
|
|
704
801
|
def install_from_metadata(metadata_url: str, internal: bool, install_dir: str = '.'):
|
705
802
|
try:
|
803
|
+
tag = None
|
804
|
+
|
706
805
|
if metadata_url.startswith("gh:"):
|
707
|
-
metadata_url = _resolve_github_metadata_url(metadata_url)
|
806
|
+
metadata_url, tag = _resolve_github_metadata_url(metadata_url)
|
708
807
|
internal = False
|
709
808
|
|
710
809
|
metadata, _ = _download_and_validate_metadata(metadata_url, internal)
|
@@ -712,7 +811,7 @@ def install_from_metadata(metadata_url: str, internal: bool, install_dir: str =
|
|
712
811
|
|
713
812
|
if _check_whether_disk_is_big_enough(metadata):
|
714
813
|
if _is_platform_compatible(metadata):
|
715
|
-
local_paths = _download_assets(metadata, metadata_url, install_dir, internal)
|
814
|
+
local_paths = _download_assets(metadata, metadata_url, install_dir, internal, tag=tag)
|
716
815
|
if len(local_paths) > 0:
|
717
816
|
_combine_multipart_files(install_dir)
|
718
817
|
_extract_archives_in_folder(install_dir, local_paths)
|
@@ -730,22 +829,32 @@ def metadata_resolver(component: str, version: str = None, tag: str = None) -> s
|
|
730
829
|
|
731
830
|
Args:
|
732
831
|
component (str): Component name (e.g., "examples.llima" or "assets/ragfps")
|
733
|
-
version (str):
|
832
|
+
version (str): Optional. If not provided, auto-detect from /etc/build.
|
734
833
|
tag (str): Optional tag to use (e.g., "dev")
|
735
834
|
|
736
835
|
Returns:
|
737
836
|
str: Fully qualified metadata URL
|
738
837
|
"""
|
838
|
+
|
739
839
|
if tag:
|
740
840
|
metadata_name = f"metadata-{tag}.json"
|
741
841
|
else:
|
742
842
|
metadata_name = "metadata.json"
|
743
843
|
|
844
|
+
# --- Asset case, assets are SDK version agnostic ---
|
744
845
|
if component.startswith("assets/"):
|
745
846
|
return f"https://docs.sima.ai/{component}/{metadata_name}"
|
746
847
|
|
848
|
+
# --- Auto-detect SDK version if missing ---
|
747
849
|
if not version:
|
748
|
-
|
850
|
+
core_version, _ = get_sima_build_version()
|
851
|
+
if core_version:
|
852
|
+
version = core_version
|
853
|
+
else:
|
854
|
+
raise ValueError(
|
855
|
+
"Version (-v) is required and could not be auto-detected "
|
856
|
+
"from /etc/build or /etc/buildinfo."
|
857
|
+
)
|
749
858
|
|
750
859
|
sdk_path = f"SDK{version}"
|
751
860
|
return f"https://docs.sima.ai/pkg_downloads/{sdk_path}/{component}/{metadata_name}"
|
@@ -8,6 +8,7 @@ class MetadataValidationError(Exception):
|
|
8
8
|
|
9
9
|
VALID_TYPES = {"board", "palette", "host"}
|
10
10
|
VALID_OS = {"linux", "windows", "mac"}
|
11
|
+
VALID_DEVKIT_SW = {"yocto", "elxr"}
|
11
12
|
|
12
13
|
def validate_metadata(data: dict):
|
13
14
|
# Top-level required fields
|
@@ -34,6 +35,15 @@ def validate_metadata(data: dict):
|
|
34
35
|
if not isinstance(platform["compatible_with"], list):
|
35
36
|
raise MetadataValidationError(f"'compatible_with' must be a list in entry {i}")
|
36
37
|
|
38
|
+
# ✅ Check optional devkit_sw
|
39
|
+
if "devkit_sw" in platform:
|
40
|
+
devkit_sw_value = platform["devkit_sw"].lower()
|
41
|
+
if devkit_sw_value not in VALID_DEVKIT_SW:
|
42
|
+
raise MetadataValidationError(
|
43
|
+
f"Invalid 'devkit_sw' value '{platform['devkit_sw']}' in platform entry {i}. "
|
44
|
+
f"Must be one of {VALID_DEVKIT_SW}"
|
45
|
+
)
|
46
|
+
|
37
47
|
if "os" in platform:
|
38
48
|
if not isinstance(platform["os"], list):
|
39
49
|
raise MetadataValidationError(f"'os' must be a list in entry {i}")
|
@@ -81,7 +91,6 @@ def validate_metadata(data: dict):
|
|
81
91
|
)
|
82
92
|
|
83
93
|
try:
|
84
|
-
# Extract number (e.g., "30GB" → 30.0)
|
85
94
|
float(size_str[:-2].strip())
|
86
95
|
except ValueError:
|
87
96
|
raise MetadataValidationError(
|
@@ -135,4 +144,4 @@ def main():
|
|
135
144
|
sys.exit(1)
|
136
145
|
|
137
146
|
if __name__ == "__main__":
|
138
|
-
main()
|
147
|
+
main()
|
sima_cli/model_zoo/model.py
CHANGED
@@ -4,6 +4,7 @@ import requests
|
|
4
4
|
import click
|
5
5
|
import os
|
6
6
|
import yaml
|
7
|
+
from InquirerPy import inquirer
|
7
8
|
from urllib.parse import urlparse
|
8
9
|
from rich import print
|
9
10
|
from rich.table import Table
|
@@ -186,35 +187,38 @@ def _download_model_internal(ver: str, model_name: str):
|
|
186
187
|
else:
|
187
188
|
click.echo("⚠️ model_path.txt exists but does not contain a valid URL.")
|
188
189
|
|
189
|
-
def _list_available_models_internal(version: str):
|
190
|
+
def _list_available_models_internal(version: str, boardtype: str):
|
191
|
+
"""
|
192
|
+
Query Artifactory for available models for the given SDK version.
|
193
|
+
Display them in an interactive menu with an 'Exit' option.
|
194
|
+
Apply boardtype filtering:
|
195
|
+
- gen1_target* → only shown for mlsoc
|
196
|
+
- gen2_target* → only shown for modalix
|
197
|
+
- others → always shown
|
198
|
+
"""
|
190
199
|
repo_path = f"SiMaCLI-SDK-Releases/{version}-Release/modelzoo_edgematic"
|
191
200
|
aql_query = f"""
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
}}).include("repo", "path", "name")
|
199
|
-
""".strip()
|
201
|
+
items.find({{
|
202
|
+
"repo": "sima-qa-releases",
|
203
|
+
"path": {{"$match": "{repo_path}/*"}},
|
204
|
+
"type": "folder"
|
205
|
+
}}).include("repo","path","name")
|
206
|
+
""".strip()
|
200
207
|
|
201
208
|
aql_url = f"{ARTIFACTORY_BASE_URL}/api/search/aql"
|
202
|
-
print(aql_url)
|
203
209
|
headers = {
|
204
210
|
"Content-Type": "text/plain",
|
205
211
|
"Authorization": f"Bearer {get_auth_token(internal=True)}"
|
206
212
|
}
|
207
213
|
|
208
214
|
response = requests.post(aql_url, data=aql_query, headers=headers)
|
209
|
-
|
210
215
|
if response.status_code != 200:
|
211
|
-
click.echo(f"Failed to retrieve model list
|
216
|
+
click.echo(f"❌ Failed to retrieve model list (status {response.status_code})")
|
212
217
|
click.echo(response.text)
|
213
|
-
return
|
218
|
+
return None
|
214
219
|
|
215
220
|
results = response.json().get("results", [])
|
216
|
-
|
217
|
-
base_prefix = f"SiMaCLI-SDK-Releases/{version}-Release/modelzoo_edgematic/"
|
221
|
+
base_prefix = f"{repo_path}/"
|
218
222
|
model_paths = sorted({
|
219
223
|
item["path"].replace(base_prefix, "").rstrip("/") + "/" + item["name"]
|
220
224
|
for item in results
|
@@ -222,20 +226,63 @@ def _list_available_models_internal(version: str):
|
|
222
226
|
|
223
227
|
if not model_paths:
|
224
228
|
click.echo("No models found.")
|
225
|
-
return
|
226
|
-
|
227
|
-
#
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
229
|
+
return None
|
230
|
+
|
231
|
+
# Apply boardtype filtering
|
232
|
+
filtered_models = []
|
233
|
+
for model in model_paths:
|
234
|
+
if model.startswith("gen1_target") and boardtype != "mlsoc":
|
235
|
+
continue
|
236
|
+
if model.startswith("gen2_target") and boardtype != "modalix":
|
237
|
+
continue
|
238
|
+
filtered_models.append(model)
|
239
|
+
|
240
|
+
if not filtered_models:
|
241
|
+
click.echo(f"No models found for board type '{boardtype}'.")
|
242
|
+
return None
|
243
|
+
|
244
|
+
while True:
|
245
|
+
# Add Exit option
|
246
|
+
choices = filtered_models + ["Exit"]
|
247
|
+
|
248
|
+
# Interactive selection with InquirerPy
|
249
|
+
selected_model = inquirer.fuzzy(
|
250
|
+
message=f"Select a model from version {version}:",
|
251
|
+
choices=choices,
|
252
|
+
max_height="70%",
|
253
|
+
instruction="(Use ↑↓ to navigate, / to search, Enter to select)"
|
254
|
+
).execute()
|
255
|
+
|
256
|
+
if selected_model == "Exit":
|
257
|
+
click.echo("👋 Exiting without selecting a model.")
|
258
|
+
return None
|
259
|
+
|
260
|
+
click.echo(f"✅ Selected model: {selected_model}")
|
261
|
+
|
262
|
+
# Auto-describe
|
263
|
+
_describe_model_internal(version, selected_model)
|
264
|
+
|
265
|
+
# Action menu loop
|
266
|
+
while True:
|
267
|
+
action = inquirer.select(
|
268
|
+
message=f"What do you want to do with {selected_model}?",
|
269
|
+
choices=["Download model", "Back", "Exit"],
|
270
|
+
default="Download model",
|
271
|
+
qmark="👉",
|
272
|
+
).execute()
|
273
|
+
|
274
|
+
if action == "Download model":
|
275
|
+
_download_model_internal(version, selected_model)
|
276
|
+
elif action == "Back":
|
277
|
+
break # back to model list
|
278
|
+
else: # Exit
|
279
|
+
click.echo("👋 Exiting.")
|
280
|
+
return None
|
281
|
+
|
282
|
+
def list_models(internal, ver, boardtype):
|
236
283
|
if internal:
|
237
284
|
click.echo("Model Zoo Source : SiMa Artifactory...")
|
238
|
-
return _list_available_models_internal(ver)
|
285
|
+
return _list_available_models_internal(ver, boardtype)
|
239
286
|
else:
|
240
287
|
print('External model zoo not supported yet')
|
241
288
|
|
@@ -260,4 +307,5 @@ if __name__ == "__main__":
|
|
260
307
|
print("Usage: python models.py <version>")
|
261
308
|
else:
|
262
309
|
version_arg = sys.argv[1]
|
263
|
-
|
310
|
+
boardtype = sys.argv[2]
|
311
|
+
_list_available_models_internal(version_arg, boardtype)
|
sima_cli/update/netboot.py
CHANGED
@@ -52,7 +52,9 @@ def flash_emmc(client_manager, emmc_image_paths):
|
|
52
52
|
|
53
53
|
for path in emmc_image_paths:
|
54
54
|
click.echo(f"📤 Copying {path} to {selected_ip}:{remote_dir}")
|
55
|
-
success = copy_file_to_remote_board(
|
55
|
+
success = copy_file_to_remote_board(
|
56
|
+
selected_ip, path, remote_dir, passwd=DEFAULT_PASSWORD
|
57
|
+
)
|
56
58
|
if not success:
|
57
59
|
click.echo(f"❌ Failed to copy {path} to {selected_ip}. Aborting.")
|
58
60
|
return
|
@@ -61,36 +63,44 @@ def flash_emmc(client_manager, emmc_image_paths):
|
|
61
63
|
ssh = init_ssh_session(selected_ip, password=DEFAULT_PASSWORD)
|
62
64
|
|
63
65
|
# Step a: Check if eMMC exists
|
64
|
-
check_cmd = "[ -e /dev/
|
66
|
+
check_cmd = "[ -e /dev/mmcblk0 ] || (echo '❌ /dev/mmcblk0 not found'; exit 1)"
|
65
67
|
run_remote_command(ssh, check_cmd)
|
66
68
|
|
67
|
-
# Step b:
|
68
|
-
wic_path = next((p for p in emmc_image_paths if p.endswith(".wic.gz")), None)
|
69
|
-
if not wic_path:
|
70
|
-
click.echo("❌ No .wic.gz image found in emmc_image_paths.")
|
71
|
-
return
|
72
|
-
|
73
|
-
# Step c: umount the emmc
|
69
|
+
# Step b: umount eMMC
|
74
70
|
pre_unmount_cmd = (
|
75
71
|
"sudo mount | grep mmcblk0 | awk '{print $3}' | while read mnt; do "
|
76
72
|
"sudo umount \"$mnt\"; done"
|
77
73
|
)
|
78
74
|
run_remote_command(ssh, pre_unmount_cmd)
|
79
75
|
|
80
|
-
# Step
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
76
|
+
# Step c: Decide flashing method
|
77
|
+
wic_path = next((p for p in emmc_image_paths if p.endswith(".wic.gz")), None)
|
78
|
+
img_path = next((p for p in emmc_image_paths if p.endswith(".img")), None)
|
79
|
+
|
80
|
+
if wic_path:
|
81
|
+
filename = os.path.basename(wic_path)
|
82
|
+
remote_path = f"/tmp/{filename}"
|
83
|
+
flash_cmd = f"sudo bmaptool copy {remote_path} /dev/mmcblk0"
|
84
|
+
run_remote_command(ssh, flash_cmd)
|
85
|
+
|
86
|
+
# Step d: Fix GPT for Yocto
|
87
|
+
fix_cmd = 'sudo printf "fix\n" | sudo parted ---pretend-input-tty /dev/mmcblk0 print'
|
88
|
+
run_remote_command(ssh, fix_cmd)
|
89
|
+
|
90
|
+
elif img_path:
|
91
|
+
filename = os.path.basename(img_path)
|
92
|
+
remote_path = f"/tmp/{filename}"
|
93
|
+
flash_cmd = f"sudo dd if={remote_path} of=/dev/mmcblk0 bs=4M conv=fsync status=progress"
|
94
|
+
run_remote_command(ssh, flash_cmd)
|
95
|
+
else:
|
96
|
+
click.echo("❌ No .wic.gz or .img image found in emmc_image_paths.")
|
97
|
+
return
|
89
98
|
|
90
99
|
click.echo("✅ Flash completed. Please reboot the board to boot from eMMC.")
|
91
100
|
except Exception as e:
|
92
101
|
click.echo(f"❌ Flashing failed: {e}")
|
93
102
|
|
103
|
+
|
94
104
|
class ClientManager:
|
95
105
|
"""Manages TFTP client state and monitoring."""
|
96
106
|
def __init__(self):
|
@@ -337,7 +347,7 @@ def run_cli(client_manager):
|
|
337
347
|
else:
|
338
348
|
click.echo("📭 No TFTP client requests received yet.")
|
339
349
|
elif user_input == "f":
|
340
|
-
click.echo("🔧 Initiating eMMC flash
|
350
|
+
click.echo(f"🔧 Initiating eMMC flash {emmc_image_paths}.")
|
341
351
|
flash_emmc(client_manager, emmc_image_paths)
|
342
352
|
elif user_input == "":
|
343
353
|
continue
|
@@ -347,7 +357,7 @@ def run_cli(client_manager):
|
|
347
357
|
click.echo("\n🛑 Exiting netboot session.")
|
348
358
|
return True
|
349
359
|
|
350
|
-
def setup_netboot(version: str, board: str, internal: bool = False, autoflash: bool = False, flavor: str = 'headless', rootfs: str = ''):
|
360
|
+
def setup_netboot(version: str, board: str, internal: bool = False, autoflash: bool = False, flavor: str = 'headless', rootfs: str = '', swtype: str = 'yocto'):
|
351
361
|
"""
|
352
362
|
Download and serve a bootable image for network boot over TFTP with client monitoring.
|
353
363
|
|
@@ -358,6 +368,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
|
|
358
368
|
autoflash (bool): Whether to automatically flash the devkit when networked booted. Defaults to False.
|
359
369
|
flavor (str): The software flavor, can be either headless or full.
|
360
370
|
rootfs (str): The root fs folder, which contains the .wic.gz file and the .bmap file, for custom image writing.
|
371
|
+
swtype (str): The software type, either yocto or elxr.
|
361
372
|
|
362
373
|
Raises:
|
363
374
|
RuntimeError: If the download or TFTP setup fails.
|
@@ -370,8 +381,8 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
|
|
370
381
|
exit(1)
|
371
382
|
|
372
383
|
try:
|
373
|
-
click.echo(f"⬇️ Downloading netboot image for version: {version}, board: {board}")
|
374
|
-
file_list = download_image(version, board, swtype=
|
384
|
+
click.echo(f"⬇️ Downloading netboot image for version: {version}, board: {board}, swtype: {swtype}")
|
385
|
+
file_list = download_image(version, board, swtype=swtype, internal=internal, update_type='netboot', flavor=flavor)
|
375
386
|
if not isinstance(file_list, list):
|
376
387
|
raise ValueError("Expected list of extracted files, got something else.")
|
377
388
|
extract_dir = os.path.dirname(file_list[0])
|
@@ -380,7 +391,8 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
|
|
380
391
|
# Extract specific image paths
|
381
392
|
wic_gz_file = next((f for f in file_list if f.endswith(".wic.gz")), None)
|
382
393
|
bmap_file = next((f for f in file_list if f.endswith(".wic.bmap")), None)
|
383
|
-
|
394
|
+
elxr_img_file = next((f for f in file_list if f.endswith(".img")), None)
|
395
|
+
emmc_image_paths = [p for p in [wic_gz_file, bmap_file, elxr_img_file] if p]
|
384
396
|
|
385
397
|
# Check global custom_rootfs before doing anything else
|
386
398
|
custom_rootfs = rootfs
|
@@ -391,13 +403,14 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
|
|
391
403
|
import glob
|
392
404
|
wic_gz_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.wic.gz"))), None)
|
393
405
|
bmap_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.wic.bmap"))), None)
|
406
|
+
exlr_file = next(iter(glob.glob(os.path.join(custom_rootfs, "*.img"))), None)
|
394
407
|
|
395
408
|
if not (wic_gz_file and bmap_file):
|
396
409
|
raise RuntimeError(
|
397
410
|
f"❌ custom_rootfs '{custom_rootfs}' must contain both .wic.gz and .wic.bmap files."
|
398
411
|
)
|
399
412
|
|
400
|
-
emmc_image_paths = [wic_gz_file, bmap_file]
|
413
|
+
emmc_image_paths = [wic_gz_file, bmap_file, exlr_file]
|
401
414
|
click.echo(f"📁 Using custom_rootfs: {custom_rootfs}")
|
402
415
|
|
403
416
|
click.echo(f"📁 eMMC image paths are: {emmc_image_paths}")
|