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.
@@ -8,11 +8,10 @@ import shutil
8
8
  import tarfile
9
9
  import zipfile
10
10
  import stat
11
- from urllib.parse import urlparse
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
- shutil.copytree(item, target, dirs_exist_ok=True)
46
+ _copy_dir(item, target, label)
49
47
  else:
50
- shutil.copy2(item, target)
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"⚠️ Missing plugin source: {plugin_name} in the SDK, skipping. "
118
- "It's possible that this is a custom plugin already exists in the repo"
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
- # Merge instead of deleting
126
- shutil.copytree(sdk_plugin_path, dest_plugin_path, dirs_exist_ok=True)
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
- url = f"https://api.github.com/repos/{owner}/{repo}/tarball/{ref}" if ref else f"https://api.github.com/repos/{owner}/{repo}/tarball"
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
- repo_dir = Path(dest_folder) / repo
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, None
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 raw URL for metadata.json.
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) # remove 'gh:'
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
- # Use GitHub API to fetch the metadata.json file
687
- api_url = f"https://api.github.com/repos/{owner}/{repo}/contents/metadata.json?ref={tag}"
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"token {token}"
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
- # Write metadata.json locally so downstream logic can use a file/URL
696
- local_path = os.path.join("/tmp", f"{repo}-{tag}-metadata.json")
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): Required unless component starts with "assets/"
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
- raise ValueError("Version (-v) is required for non-asset components.")
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()
@@ -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
- items.find({{
193
- "repo": "sima-qa-releases",
194
- "path": {{
195
- "$match": "{repo_path}/*"
196
- }},
197
- "type": "folder"
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. Status: {response.status_code}")
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
- # Pretty print table
228
- max_len = max(len(name) for name in model_paths)
229
- click.echo(f"{'-' * max_len}")
230
- for path in model_paths:
231
- click.echo(path.ljust(max_len))
232
-
233
- return model_paths
234
-
235
- def list_models(internal, ver):
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
- _list_available_models_internal(version_arg)
310
+ boardtype = sys.argv[2]
311
+ _list_available_models_internal(version_arg, boardtype)
@@ -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(selected_ip, path, remote_dir, passwd=DEFAULT_PASSWORD)
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/mmcblk0p1 ] || (echo '❌ /dev/mmcblk0p1 not found'; exit 1)"
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: Flash with bmaptool
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 d: bmaptool copy the image
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
87
- fix_cmd = 'sudo printf "fix\n" | sudo parted ---pretend-input-tty /dev/mmcblk0 print'
88
- run_remote_command(ssh, fix_cmd)
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 (implementation pending).")
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="yocto", internal=internal, update_type='netboot', flavor=flavor)
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
- emmc_image_paths = [p for p in [wic_gz_file, bmap_file] if p]
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}")