sima-cli 0.0.18__py3-none-any.whl → 0.0.20__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.
@@ -0,0 +1,137 @@
1
+ import subprocess
2
+ import sys
3
+ import os
4
+ import urllib.request
5
+ import tarfile
6
+ import tempfile
7
+ import json
8
+ import shutil
9
+
10
+ def run_command(command, check=True, shell=False, cwd=None):
11
+ """Run a shell command and return its output or raise an exception if it fails."""
12
+ try:
13
+ result = subprocess.run(
14
+ command,
15
+ check=check,
16
+ shell=shell,
17
+ stdout=subprocess.PIPE,
18
+ stderr=subprocess.PIPE,
19
+ text=True,
20
+ cwd=cwd
21
+ )
22
+ return result.stdout.strip(), result.stderr.strip()
23
+ except subprocess.CalledProcessError as e:
24
+ if check:
25
+ print(f"Error running command {command}: {e.stderr}")
26
+ raise
27
+ return "", str(e)
28
+
29
+ def is_bmaptool_installed():
30
+ """Check if bmaptool is installed and callable from the shell."""
31
+ try:
32
+ result = subprocess.run(
33
+ ["bmaptool", "--version"],
34
+ stdout=subprocess.PIPE,
35
+ stderr=subprocess.PIPE,
36
+ text=True,
37
+ timeout=5
38
+ )
39
+ if result.returncode == 0:
40
+ print(f"✅ bmaptool is installed: {result.stdout.strip()}")
41
+ return True
42
+ else:
43
+ print(f"⚠️ bmaptool returned non-zero exit: {result.stderr.strip()}")
44
+ return False
45
+ except (FileNotFoundError, subprocess.TimeoutExpired) as e:
46
+ print(f"❌ bmaptool is not installed or not in PATH: {e}")
47
+ return False
48
+
49
+ def clone_bmaptool_repo(temp_dir, branch_or_tag="main"):
50
+ """Clone the bmaptool repository to a temporary directory."""
51
+ repo_url = "https://github.com/yoctoproject/bmaptool.git"
52
+ print(f"Cloning bmaptool from {repo_url} (branch/tag: {branch_or_tag})...")
53
+ try:
54
+ subprocess.run(
55
+ ["git", "clone", "--depth", "1", "--branch", branch_or_tag, repo_url, temp_dir],
56
+ check=True,
57
+ capture_output=True,
58
+ text=True
59
+ )
60
+ print(f"Cloned bmaptool to {temp_dir}")
61
+ return temp_dir
62
+ except subprocess.CalledProcessError as e:
63
+ print(f"Failed to clone repository: {e.stderr}")
64
+ sys.exit(1)
65
+
66
+ def get_python_and_pip_bins():
67
+ """Get the Python and pip binaries from the active virtual environment or system."""
68
+ if "VIRTUAL_ENV" in os.environ:
69
+ venv_dir = os.environ["VIRTUAL_ENV"]
70
+ print(f"Using existing virtual environment: {venv_dir}")
71
+ if sys.platform == "win32":
72
+ python_bin = os.path.join(venv_dir, "Scripts", "python.exe")
73
+ pip_bin = os.path.join(venv_dir, "Scripts", "pip.exe")
74
+ else:
75
+ python_bin = os.path.join(venv_dir, "bin", "python3")
76
+ pip_bin = os.path.join(venv_dir, "bin", "pip3")
77
+ else:
78
+ print("No virtual environment detected; using system Python.")
79
+ python_bin = shutil.which("python3") or sys.executable
80
+ pip_bin = shutil.which("pip3") or "pip3"
81
+ return python_bin, pip_bin
82
+
83
+ def install_bmaptool(temp_dir):
84
+ """Install bmaptool from the cloned repository."""
85
+ try:
86
+ subprocess.run(
87
+ [sys.executable, "-m", "pip", "install", temp_dir],
88
+ check=True,
89
+ capture_output=True,
90
+ text=True
91
+ )
92
+ print("Installed bmaptool successfully")
93
+ except subprocess.CalledProcessError as e:
94
+ print(f"Failed to install bmaptool: {e.stderr}")
95
+ sys.exit(1)
96
+
97
+ def verify_installation(python_bin):
98
+ """Verify bmaptool installation by running python -m bmaptool --version."""
99
+ stdout, stderr = run_command([python_bin, "-m", "bmaptool", "--version"])
100
+ if stdout:
101
+ print(f"bmaptool installation verified: {stdout}")
102
+ else:
103
+ print(f"Verification failed: {stderr}")
104
+ sys.exit(1)
105
+
106
+ def check_bmap_and_install():
107
+ # Get Python and pip binaries from the active virtual environment or system
108
+ python_bin, _ = get_python_and_pip_bins()
109
+
110
+ # Check if bmaptool is already installed
111
+ if is_bmaptool_installed():
112
+ return
113
+
114
+ # Create a temporary directory
115
+ temp_dir = "bmaptool_temp"
116
+ with tempfile.TemporaryDirectory() as temp_dir:
117
+ if os.path.exists(temp_dir):
118
+ shutil.rmtree(temp_dir)
119
+ os.makedirs(temp_dir)
120
+
121
+ clone_bmaptool_repo(temp_dir, branch_or_tag="main") # Or specify a tag like "v3.8"
122
+ install_bmaptool(temp_dir)
123
+
124
+ # Verify installation
125
+ verify_installation(python_bin)
126
+
127
+ # Provide usage instructions
128
+ if "VIRTUAL_ENV" in os.environ:
129
+ print(f"\nbmaptool is installed in the virtual environment at {os.environ['VIRTUAL_ENV']}")
130
+ print("The virtual environment is already active.")
131
+ print("Run 'bmaptool --help' to see available commands.")
132
+ else:
133
+ print("\nbmaptool is installed in the system Python environment.")
134
+ print("Run 'bmaptool --help' to see available commands.")
135
+
136
+ if __name__ == "__main__":
137
+ check_bmap_and_install()
@@ -0,0 +1,339 @@
1
+ import click
2
+ import platform
3
+ import subprocess
4
+ import sys
5
+ import os
6
+ import select
7
+ import re
8
+ import threading
9
+ from tftpy import TftpServer
10
+
11
+ try:
12
+ from tqdm import tqdm
13
+ except ImportError:
14
+ tqdm = None
15
+
16
+
17
+ def list_removable_devices():
18
+ system = platform.system()
19
+
20
+ if system == "Linux":
21
+ return get_linux_removable()
22
+ elif system == "Darwin":
23
+ return get_macos_removable()
24
+ elif system == "Windows":
25
+ return get_windows_removable()
26
+ else:
27
+ click.echo(f"❌ Unsupported platform: {system}")
28
+ return []
29
+
30
+ # Linux: Use lsblk to find removable drives
31
+ def get_linux_removable():
32
+ try:
33
+ output = subprocess.check_output(["lsblk", "-o", "NAME,RM,SIZE,MOUNTPOINT", "-J"]).decode()
34
+ import json
35
+ data = json.loads(output)
36
+ devices = []
37
+ for block in data['blockdevices']:
38
+ if block.get('rm') == True and block.get('mountpoint') is None:
39
+ devices.append({
40
+ "name": block['name'],
41
+ "size": block['size'],
42
+ "path": f"/dev/{block['name']}"
43
+ })
44
+ return devices
45
+ except Exception:
46
+ return []
47
+
48
+ # macOS: Use diskutil
49
+ def get_macos_removable():
50
+ try:
51
+ output = subprocess.check_output(["diskutil", "list"], text=True)
52
+ devices = []
53
+
54
+ candidate_disks = [
55
+ line.split()[0]
56
+ for line in output.splitlines()
57
+ if line.startswith("/dev/disk")
58
+ ]
59
+
60
+ for disk in candidate_disks:
61
+ info = subprocess.check_output(["diskutil", "info", disk], text=True)
62
+ is_removable = False
63
+ is_disk_image = False
64
+ size = "Unknown"
65
+ device_name = ''
66
+
67
+ for info_line in info.splitlines():
68
+ if "Secure Digital" in info_line or "USB" in info_line:
69
+ is_removable = True
70
+ elif "Disk Size" in info_line and "(" in info_line:
71
+ size = info_line.split("(")[1].split()[0]
72
+ elif "Volume Name" in info_line:
73
+ volume_name = info_line.split(":")[-1].strip() or "Unknown"
74
+ elif "Device / Media Name" in info_line:
75
+ is_disk_image = ('Disk Image' in info_line)
76
+ device_name = info_line.split(":")[-1].strip() or "Unknown"
77
+
78
+ # switch to raw device to speed up dd performance
79
+ if is_removable and not is_disk_image:
80
+ devices.append({
81
+ "name": device_name,
82
+ "size": round(int(size) / (1024 ** 3), 0),
83
+ "path": disk.replace('/disk', '/rdisk')
84
+ })
85
+
86
+ return devices
87
+ except Exception as e:
88
+ click.echo(f"Failed to detect removable devices on macOS: {e}")
89
+ return []
90
+
91
+ # Windows: Use wmic or powershell
92
+ def get_windows_removable():
93
+ try:
94
+ output = subprocess.check_output(
95
+ ['powershell', '-Command',
96
+ 'Get-WmiObject Win32_DiskDrive | Where { $_.MediaType -match "Removable" } | '
97
+ 'Select-Object DeviceID,Model,Size | ConvertTo-Json']
98
+ ).decode()
99
+ import json
100
+ parsed = json.loads(output)
101
+ if not isinstance(parsed, list):
102
+ parsed = [parsed]
103
+ devices = []
104
+ for d in parsed:
105
+ size_gb = int(d.get("Size", 0)) // (1024 ** 3)
106
+ devices.append({
107
+ "name": d.get("Model", "Removable Drive"),
108
+ "size": f"{size_gb} GB",
109
+ "path": d["DeviceID"]
110
+ })
111
+ return devices
112
+ except Exception:
113
+ return []
114
+
115
+ def check_dd_installed():
116
+ """Check if dd is installed on the system."""
117
+ try:
118
+ subprocess.run(["which", "dd"], capture_output=True, check=True)
119
+ return True
120
+ except (subprocess.CalledProcessError, FileNotFoundError):
121
+ return False
122
+
123
+ def unmount_device(device_path):
124
+ """Unmount the device using platform-specific commands."""
125
+ system = platform.system()
126
+ try:
127
+ if system == "Darwin": # macOS
128
+ subprocess.run(["diskutil", "unmountDisk", device_path], check=True, capture_output=True, text=True)
129
+ click.echo(f"✅ Unmounted {device_path} on macOS")
130
+ elif system == "Linux":
131
+ result = subprocess.run(["umount", device_path], capture_output=True, text=True)
132
+ if result.returncode == 0:
133
+ click.echo(f"✅ Unmounted {device_path} on Linux")
134
+ elif "not mounted" in result.stderr.lower():
135
+ click.echo(f"ℹ️ {device_path} was not mounted. Continuing.")
136
+ else:
137
+ click.echo(f"❌ Failed to unmount {device_path}: {result.stderr.strip()}")
138
+ sys.exit(1)
139
+ else:
140
+ click.echo(f"❌ Unsupported platform: {system}. Cannot unmount {device_path}.")
141
+ sys.exit(1)
142
+ except Exception as e:
143
+ click.echo(f"❌ Unexpected error while unmounting {device_path}: {e}")
144
+ sys.exit(1)
145
+
146
+ def _require_sudo():
147
+ try:
148
+ # This will prompt for password if necessary and cache it for a few minutes
149
+ click.echo("✅ Running this command requires sudo access.")
150
+ subprocess.run(["sudo", "-v"], check=True)
151
+ except subprocess.CalledProcessError:
152
+ click.echo("❌ Sudo authentication failed.")
153
+ sys.exit(1)
154
+
155
+ def copy_image_to_device(image_path, device_path):
156
+ """Copy the image file to the device using dd with 16M block size."""
157
+ # Get file size for progress calculation
158
+ file_size = os.path.getsize(image_path)
159
+ click.echo(f"ℹ️ Running 'sudo dd' to copy {image_path} to {device_path}")
160
+
161
+ # Debug: Log raw dd output to a file for diagnosis
162
+ debug_log = "dd_output.log"
163
+ click.echo(f"ℹ️ Logging raw dd output to {debug_log} for debugging.")
164
+ _require_sudo()
165
+
166
+ dd_command = ["sudo", "dd", f"if={image_path}", f"of={device_path}", "bs=16M", "status=progress"]
167
+ try:
168
+ # Start dd process with unbuffered output
169
+ process = subprocess.Popen(
170
+ dd_command,
171
+ stdout=subprocess.PIPE,
172
+ stderr=subprocess.PIPE,
173
+ text=True,
174
+ bufsize=1,
175
+ universal_newlines=True,
176
+ errors="replace"
177
+ )
178
+
179
+ # Regex to parse dd progress (more robust to handle variations)
180
+ pattern = re.compile(
181
+ r"(?:dd:\s*)?(?P<bytes>\d+)\s+bytes(?:\s+\(.*?\))?\s+(?:transferred|copied),?\s+[\d\.]+\s*s?,?\s+[\d\.]+\s+MB/s",
182
+ re.IGNORECASE
183
+ )
184
+
185
+ # Initialize tqdm progress bar
186
+ with tqdm(total=file_size, unit="B", unit_scale=True, desc="Copying", ncols=100) as pbar:
187
+ with open(debug_log, "w") as log_file:
188
+ while process.poll() is None:
189
+ rlist, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
190
+ for stream in rlist:
191
+ line = stream.readline().strip()
192
+ if line:
193
+ log_file.write(f"{line}\n")
194
+ log_file.flush()
195
+ match = pattern.search(line)
196
+ if match:
197
+ bytes_transferred = int(match.group(1))
198
+ pbar.n = min(bytes_transferred, file_size)
199
+ pbar.refresh()
200
+ elif line:
201
+ click.echo(f"⚠️ dd: {line}") # Show other messages (e.g., errors)
202
+
203
+ # Capture remaining output and check for errors
204
+ stdout, stderr = process.communicate()
205
+ with open(debug_log, "a") as log_file:
206
+ if stdout.strip():
207
+ log_file.write(f"Final stdout: {stdout.strip()}\n")
208
+ if stderr.strip():
209
+ log_file.write(f"Final stderr: {stderr.strip()}\n")
210
+
211
+ if process.returncode != 0:
212
+ click.echo(f"❌ Failed to copy {image_path} to {device_path}: {stderr}")
213
+ click.echo(f"ℹ️ Check {debug_log} for raw dd output.")
214
+ sys.exit(1)
215
+
216
+ click.echo(f"✅ Successfully copied {image_path} to {device_path}")
217
+ subprocess.run(["sync"], check=True)
218
+ click.echo("✅ Synced data to device")
219
+ except subprocess.CalledProcessError as e:
220
+ click.echo(f"❌ Failed to copy {image_path} to {device_path}: {e.stderr}")
221
+ click.echo(f"ℹ️ Check {debug_log} for raw dd output.")
222
+ sys.exit(1)
223
+ except FileNotFoundError:
224
+ click.echo("❌ 'dd' not found. Ensure both are installed and accessible.")
225
+ sys.exit(1)
226
+
227
+ def write_bootimg(image_path):
228
+ """Write a boot image to a removable device."""
229
+ # Step 1: Validate image file
230
+ if not os.path.isfile(image_path):
231
+ click.echo(f"❌ Image file {image_path} does not exist.")
232
+ sys.exit(1)
233
+
234
+ click.echo(f"✅ Valid image file: {image_path}")
235
+
236
+ # Step 2: Check if dd is installed
237
+ if not check_dd_installed():
238
+ click.echo("⚠️ 'dd' is not installed on this system.")
239
+ if platform.system() == "Darwin":
240
+ click.echo("ℹ️ On macOS, 'dd' is included by default. Check your PATH or system configuration.")
241
+ elif platform.system() == "Linux":
242
+ click.echo("ℹ️ On Linux, install 'dd' using your package manager (e.g., 'sudo apt install coreutils' on Debian/Ubuntu).")
243
+ else:
244
+ click.echo("ℹ️ Please install 'dd' for your system.")
245
+ sys.exit(1)
246
+ click.echo("✅ 'dd' is installed")
247
+
248
+ # Step 3: Detect removable devices
249
+ devices = list_removable_devices() # Assumes this function exists
250
+ if not devices:
251
+ click.echo("⚠️ No removable devices detected. Please plug in your USB drive or SD card.")
252
+ sys.exit(1)
253
+
254
+ # Step 4: Display devices
255
+ click.echo("\n🔍 Detected removable devices:")
256
+ for i, dev in enumerate(devices):
257
+ click.echo(f" [{i}] Name: {dev['name']}, Size: {dev['size']} GB, Path: {dev['path']}")
258
+
259
+ # Step 5: Select device
260
+ selected_device = None
261
+ if len(devices) == 1:
262
+ confirm = input(f"\n✅ Do you want to use device {devices[0]['path']}? (y/N): ").strip().lower()
263
+ if confirm == 'y':
264
+ selected_device = devices[0]
265
+ else:
266
+ click.echo("❌ Operation cancelled by user.")
267
+ sys.exit(0)
268
+ else:
269
+ try:
270
+ selection = int(input("\n🔢 Multiple devices found. Enter the number of the device to use: ").strip())
271
+ if 0 <= selection < len(devices):
272
+ selected_device = devices[selection]
273
+ else:
274
+ click.echo("❌ Invalid selection.")
275
+ sys.exit(1)
276
+ except ValueError:
277
+ click.echo("❌ Invalid input. Please enter a number.")
278
+ sys.exit(1)
279
+
280
+ click.echo(f"\n🚀 Proceeding with device: {selected_device['path']}")
281
+
282
+ # Step 6: Unmount the selected device
283
+ unmount_device(selected_device['path'])
284
+
285
+ # Step 7: Copy the image to the device
286
+ copy_image_to_device(image_path, selected_device['path'])
287
+
288
+ # Step 8: Eject the device (platform-dependent)
289
+ try:
290
+ if platform.system() == "Darwin":
291
+ subprocess.run(["diskutil", "eject", selected_device['path']], check=True, capture_output=True, text=True)
292
+ click.echo(f"✅ Ejected {selected_device['path']} on macOS")
293
+ elif platform.system() == "Linux":
294
+ click.echo("ℹ️ Linux does not require explicit eject. Device is ready.")
295
+ else:
296
+ click.echo("ℹ️ Please manually eject the device if required.")
297
+ except subprocess.CalledProcessError as e:
298
+ click.echo(f"⚠️ Failed to eject {selected_device['path']}: {e.stderr}")
299
+ click.echo("ℹ️ Please manually eject the device.")
300
+
301
+
302
+ def write_image(version: str, board: str, swtype: str, internal: bool = False):
303
+ """
304
+ Download and write a bootable firmware image to a removable storage device.
305
+
306
+ Parameters:
307
+ version (str): Firmware version to download (e.g., "1.6.0").
308
+ board (str): Target board type, e.g., "modalix" or "mlsoc".
309
+ swtype (str): Software image type, e.g., "yocto" or "exlr".
310
+ internal (bool): Whether to use internal download sources. Defaults to False.
311
+
312
+ Raises:
313
+ RuntimeError: If the download or write process fails.
314
+ """
315
+ try:
316
+ click.echo(f"⬇️ Downloading boot image for version: {version}, board: {board}, swtype: {swtype}")
317
+ file_list = download_image(version, board, swtype, internal, update_type='bootimg')
318
+ if not isinstance(file_list, list):
319
+ raise ValueError("Expected list of extracted files, got something else.")
320
+
321
+ wic_file = next((f for f in file_list if f.endswith(".wic")), None)
322
+ if not wic_file:
323
+ raise FileNotFoundError("No .wic image file found after extraction.")
324
+
325
+ except Exception as e:
326
+ raise RuntimeError(f"❌ Failed to download image: {e}")
327
+
328
+ try:
329
+ click.echo(f"📝 Writing image to removable media: {wic_file}")
330
+ write_bootimg(wic_file)
331
+ except Exception as e:
332
+ raise RuntimeError(f"❌ Failed to write image: {e}")
333
+
334
+ if __name__ == "__main__":
335
+ if len(sys.argv) != 2:
336
+ click.echo("❌ Usage: python write_bootimg.py <image_file>")
337
+ sys.exit(1)
338
+
339
+ write_image('1.7.0', 'modalix', 'davinci', True)