replx-agent 2.0.0__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.
Potentially problematic release.
This version of replx-agent might be problematic. Click here for more details.
- replx_agent/__init__.py +3 -0
- replx_agent/__main__.py +5 -0
- replx_agent/commands.py +131 -0
- replx_agent/helpers/__init__.py +3 -0
- replx_agent/helpers/scanner.py +308 -0
- replx_agent/pkg/__init__.py +11 -0
- replx_agent/pkg/compiler.py +153 -0
- replx_agent/pkg/registry.py +387 -0
- replx_agent/pkg/service.py +690 -0
- replx_agent/pkg/store.py +83 -0
- replx_agent/protocol/__init__.py +11 -0
- replx_agent/protocol/repl.py +1029 -0
- replx_agent/protocol/storage.py +1018 -0
- replx_agent/rpc/__init__.py +3 -0
- replx_agent/rpc/protocol.py +220 -0
- replx_agent/server/__init__.py +3 -0
- replx_agent/server/__main__.py +4 -0
- replx_agent/server/command_dispatcher.py +49 -0
- replx_agent/server/connection_manager.py +654 -0
- replx_agent/server/core.py +1221 -0
- replx_agent/server/handlers/__init__.py +24 -0
- replx_agent/server/handlers/exec.py +553 -0
- replx_agent/server/handlers/filesystem.py +383 -0
- replx_agent/server/handlers/firmware.py +118 -0
- replx_agent/server/handlers/i2c.py +23 -0
- replx_agent/server/handlers/package.py +182 -0
- replx_agent/server/handlers/repl.py +188 -0
- replx_agent/server/handlers/session.py +205 -0
- replx_agent/server/handlers/spi.py +23 -0
- replx_agent/server/handlers/transfer.py +600 -0
- replx_agent/server/handlers/uart.py +32 -0
- replx_agent/server/session_manager.py +391 -0
- replx_agent/server/tcp.py +207 -0
- replx_agent/terminal.py +566 -0
- replx_agent/transport/__init__.py +32 -0
- replx_agent/transport/serial.py +220 -0
- replx_agent/utils/__init__.py +95 -0
- replx_agent/utils/constants.py +78 -0
- replx_agent/utils/device_info.py +183 -0
- replx_agent/utils/exceptions.py +31 -0
- replx_agent-2.0.0.dist-info/METADATA +74 -0
- replx_agent-2.0.0.dist-info/RECORD +46 -0
- replx_agent-2.0.0.dist-info/WHEEL +5 -0
- replx_agent-2.0.0.dist-info/entry_points.txt +2 -0
- replx_agent-2.0.0.dist-info/licenses/LICENSE +21 -0
- replx_agent-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"""Package store orchestration for the agent.
|
|
2
|
+
|
|
3
|
+
Pure logic (no UI, no board I/O): download planning/execution against the
|
|
4
|
+
GitHub registry, install batch preparation (compile to staging), store
|
|
5
|
+
cleanup, store status and typehints enumeration. Board writes are done by
|
|
6
|
+
the command handler using the prepared batch.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import hashlib
|
|
11
|
+
import shutil
|
|
12
|
+
import urllib.error
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
|
+
from typing import Callable, Optional
|
|
15
|
+
|
|
16
|
+
from replx_agent.utils import device_name_to_path
|
|
17
|
+
from replx_agent.utils.exceptions import PkgError
|
|
18
|
+
from .store import StoreManager
|
|
19
|
+
from .registry import InstallHelper, SearchHelper, RegistryHelper
|
|
20
|
+
from .compiler import CompilerHelper
|
|
21
|
+
|
|
22
|
+
ProgressFn = Callable[[dict], None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_local_pkg_version(local_meta: dict, *, scope: str, target: str,
|
|
26
|
+
source_path: str, pkg_name: str) -> tuple[float, bool]:
|
|
27
|
+
if not local_meta or not isinstance(local_meta, dict):
|
|
28
|
+
return 0.0, True
|
|
29
|
+
|
|
30
|
+
local_packages = local_meta.get("packages", {})
|
|
31
|
+
if not local_packages or not isinstance(local_packages, dict):
|
|
32
|
+
return 0.0, True
|
|
33
|
+
|
|
34
|
+
if not scope or not target or not source_path or not pkg_name:
|
|
35
|
+
return 0.0, True
|
|
36
|
+
|
|
37
|
+
base_key = f"{scope}:{target}:{pkg_name}"
|
|
38
|
+
|
|
39
|
+
matching_versions = []
|
|
40
|
+
for key, meta in local_packages.items():
|
|
41
|
+
if not isinstance(key, str) or not isinstance(meta, dict):
|
|
42
|
+
continue
|
|
43
|
+
if key == base_key or key.startswith(base_key + "@"):
|
|
44
|
+
meta_source = meta.get("source") or meta.get("typehint") or ""
|
|
45
|
+
if meta_source == source_path:
|
|
46
|
+
matching_versions.append(RegistryHelper.get_version(meta))
|
|
47
|
+
if matching_versions:
|
|
48
|
+
return max(matching_versions), False
|
|
49
|
+
|
|
50
|
+
if base_key in local_packages:
|
|
51
|
+
meta = local_packages[base_key]
|
|
52
|
+
if isinstance(meta, dict):
|
|
53
|
+
meta_source = meta.get("source") or meta.get("typehint") or ""
|
|
54
|
+
if not source_path or not meta_source or meta_source == source_path:
|
|
55
|
+
return RegistryHelper.get_version(meta), False
|
|
56
|
+
|
|
57
|
+
if scope == "device":
|
|
58
|
+
best = None
|
|
59
|
+
for key, meta in local_packages.items():
|
|
60
|
+
if not isinstance(key, str):
|
|
61
|
+
continue
|
|
62
|
+
if key.startswith(base_key + "@"):
|
|
63
|
+
v = RegistryHelper.get_version(meta)
|
|
64
|
+
best = v if best is None else max(best, v)
|
|
65
|
+
if best is not None:
|
|
66
|
+
return best, False
|
|
67
|
+
|
|
68
|
+
prefix_lower = f"{scope}:{target}:".lower()
|
|
69
|
+
for key, meta in local_packages.items():
|
|
70
|
+
if not isinstance(key, str) or not isinstance(meta, dict):
|
|
71
|
+
continue
|
|
72
|
+
if key.lower().startswith(prefix_lower) and meta.get("source") == source_path:
|
|
73
|
+
return RegistryHelper.get_version(meta), False
|
|
74
|
+
|
|
75
|
+
return 0.0, True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def compute_search_rows(core: str, device: str, owner: str, repo: str, ref: str,
|
|
79
|
+
query: Optional[str] = None) -> dict:
|
|
80
|
+
try:
|
|
81
|
+
remote = StoreManager.load_remote_meta(owner, repo, ref)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise PkgError(f"Failed to load remote registry: {e}")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
local = StoreManager.load_local_meta()
|
|
87
|
+
except Exception:
|
|
88
|
+
local = {}
|
|
89
|
+
|
|
90
|
+
cores, devices = RegistryHelper.root_sections(remote)
|
|
91
|
+
|
|
92
|
+
def status_label(remote_ver: float, local_ver_: float, missing_local: bool) -> str:
|
|
93
|
+
if missing_local:
|
|
94
|
+
return "NEW"
|
|
95
|
+
if remote_ver > (local_ver_ or 0.0):
|
|
96
|
+
return "UPD"
|
|
97
|
+
return ""
|
|
98
|
+
|
|
99
|
+
def local_ver(source_path: str, target: str, scope: str) -> tuple[float, bool]:
|
|
100
|
+
if not source_path or not target:
|
|
101
|
+
return 0.0, True
|
|
102
|
+
|
|
103
|
+
filename = source_path.split("/")[-1] if "/" in source_path else source_path
|
|
104
|
+
name = filename.replace(".py", "").replace(".pyi", "").replace("__init__", "")
|
|
105
|
+
if not name and "/" in source_path:
|
|
106
|
+
parts = source_path.split("/")
|
|
107
|
+
name = parts[-2] if len(parts) >= 2 else ""
|
|
108
|
+
|
|
109
|
+
return find_local_pkg_version(local, scope=scope, target=target,
|
|
110
|
+
source_path=source_path, pkg_name=name)
|
|
111
|
+
|
|
112
|
+
def add_core_rows(core_name: str, rows: list):
|
|
113
|
+
for relpath, pkg_meta in RegistryHelper.walk_files_for_core(remote, core_name, "src"):
|
|
114
|
+
if not relpath.endswith(".py"):
|
|
115
|
+
continue
|
|
116
|
+
rver = RegistryHelper.get_version(pkg_meta)
|
|
117
|
+
source = pkg_meta.get("source", "")
|
|
118
|
+
lver, missing = local_ver(source, core_name, "core")
|
|
119
|
+
stat = status_label(rver, lver, missing)
|
|
120
|
+
pkg_name = pkg_meta.get("source", "").split("/")[-1].replace(".py", "")
|
|
121
|
+
display_path = f"src/{relpath}"
|
|
122
|
+
if relpath.endswith("/__init__.py"):
|
|
123
|
+
display_path = display_path.replace("/__init__.py", "/")
|
|
124
|
+
rows.append(("core", core_name, stat, f"{rver:.1f}", display_path, pkg_name))
|
|
125
|
+
|
|
126
|
+
def add_device_rows(dev_name: str, rows: list):
|
|
127
|
+
for relpath, pkg_meta in RegistryHelper.walk_files_for_device(remote, dev_name, "src", include_submodules=True):
|
|
128
|
+
if not (relpath.endswith(".py") or relpath.endswith(".bin")):
|
|
129
|
+
continue
|
|
130
|
+
rver = RegistryHelper.get_version(pkg_meta)
|
|
131
|
+
source = pkg_meta.get("_parent_source") or pkg_meta.get("source", "")
|
|
132
|
+
lver, missing = local_ver(source, dev_name, "device")
|
|
133
|
+
stat = status_label(rver, lver, missing)
|
|
134
|
+
pkg_name = pkg_meta.get("source", "").split("/")[-1].replace(".py", "")
|
|
135
|
+
display_path = f"src/{relpath}"
|
|
136
|
+
if relpath.endswith("/__init__.py"):
|
|
137
|
+
display_path = display_path.replace("/__init__.py", "/")
|
|
138
|
+
elif relpath == "__init__.py":
|
|
139
|
+
src_parts = source.split("/")
|
|
140
|
+
if len(src_parts) >= 2 and src_parts[0] == "device":
|
|
141
|
+
pkg_folder = src_parts[1].lstrip("_")
|
|
142
|
+
display_path = f"src/{pkg_folder}/"
|
|
143
|
+
rows.append(("device", dev_name, stat, f"{rver:.1f}", display_path, pkg_name))
|
|
144
|
+
|
|
145
|
+
cur_core = (core or "").strip()
|
|
146
|
+
cur_dev = (device or "").strip()
|
|
147
|
+
cur_dev_key = SearchHelper.key_ci(devices, device_name_to_path(cur_dev)) if cur_dev else None
|
|
148
|
+
cur_core_key = SearchHelper.key_ci(cores, cur_core) if cur_core else None
|
|
149
|
+
if not cur_core_key:
|
|
150
|
+
cur_dev_key = None
|
|
151
|
+
|
|
152
|
+
rows: list[tuple[str, str, str, str, str, str]] = []
|
|
153
|
+
|
|
154
|
+
if query:
|
|
155
|
+
dkey = SearchHelper.key_ci(devices, device_name_to_path(query))
|
|
156
|
+
ckey = SearchHelper.key_ci(cores, query)
|
|
157
|
+
|
|
158
|
+
if dkey and cur_dev_key and dkey == cur_dev_key:
|
|
159
|
+
add_device_rows(dkey, rows)
|
|
160
|
+
elif ckey and cur_core_key and ckey == cur_core_key:
|
|
161
|
+
add_core_rows(ckey, rows)
|
|
162
|
+
else:
|
|
163
|
+
q = query.lower()
|
|
164
|
+
temp_rows = []
|
|
165
|
+
|
|
166
|
+
if cur_core_key:
|
|
167
|
+
add_core_rows(cur_core_key, temp_rows)
|
|
168
|
+
if cur_dev_key and cur_dev_key != cur_core_key:
|
|
169
|
+
add_device_rows(cur_dev_key, temp_rows)
|
|
170
|
+
|
|
171
|
+
for scope, target, stat, ver_str, shown_path, pkg_name in temp_rows:
|
|
172
|
+
if (q in pkg_name.lower()) or (q in shown_path.lower()):
|
|
173
|
+
rows.append((scope, target, stat, ver_str, shown_path, pkg_name))
|
|
174
|
+
else:
|
|
175
|
+
if cur_core_key:
|
|
176
|
+
add_core_rows(cur_core_key, rows)
|
|
177
|
+
if cur_dev_key and cur_dev_key != cur_core_key:
|
|
178
|
+
add_device_rows(cur_dev_key, rows)
|
|
179
|
+
|
|
180
|
+
def row_key(r):
|
|
181
|
+
scope_order = 0 if r[0] == "core" else 1
|
|
182
|
+
return (scope_order, r[1].lower(), r[4].lower())
|
|
183
|
+
|
|
184
|
+
rows.sort(key=row_key)
|
|
185
|
+
|
|
186
|
+
return {"rows": [list(r) for r in rows]}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def download(core: str, device: str, owner: str, repo: str, ref: str,
|
|
190
|
+
scope_filter: Optional[str] = None,
|
|
191
|
+
file_filter: Optional[str] = None,
|
|
192
|
+
progress: Optional[ProgressFn] = None) -> dict:
|
|
193
|
+
StoreManager.ensure_home_store()
|
|
194
|
+
|
|
195
|
+
dev = (device or "").strip()
|
|
196
|
+
core = (core or "").strip()
|
|
197
|
+
|
|
198
|
+
if not core:
|
|
199
|
+
raise PkgError("Failed to get device info from connected board.")
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
remote = StoreManager.load_remote_meta(owner, repo, ref)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise PkgError(f"Failed to load remote meta: {e}")
|
|
205
|
+
|
|
206
|
+
cores, devices = RegistryHelper.root_sections(remote)
|
|
207
|
+
|
|
208
|
+
if scope_filter not in (None, "core", "device"):
|
|
209
|
+
raise PkgError(f"Invalid scope_filter: {scope_filter}")
|
|
210
|
+
|
|
211
|
+
include_core = scope_filter in (None, "core")
|
|
212
|
+
include_device = scope_filter in (None, "device")
|
|
213
|
+
|
|
214
|
+
core_exists_remote = core in cores if core else False
|
|
215
|
+
device_exists_remote = (device_name_to_path(dev) in devices if dev else False) and (dev != core)
|
|
216
|
+
|
|
217
|
+
if not core_exists_remote:
|
|
218
|
+
return {
|
|
219
|
+
"core": core, "device": dev,
|
|
220
|
+
"core_in_registry": False,
|
|
221
|
+
"total": 0, "new": 0, "upd": 0,
|
|
222
|
+
"errors": [], "target": "",
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
with StoreManager.LOCK:
|
|
226
|
+
try:
|
|
227
|
+
local = StoreManager.load_local_meta()
|
|
228
|
+
if not isinstance(local, dict):
|
|
229
|
+
local = {}
|
|
230
|
+
except Exception:
|
|
231
|
+
local = {}
|
|
232
|
+
|
|
233
|
+
local_packages = local.setdefault("packages", {})
|
|
234
|
+
|
|
235
|
+
if "platform_cores" in remote and core:
|
|
236
|
+
remote_platform_cores = remote.get("platform_cores", {})
|
|
237
|
+
if core in remote_platform_cores:
|
|
238
|
+
if "platform_cores" not in local:
|
|
239
|
+
local["platform_cores"] = {}
|
|
240
|
+
local["platform_cores"][core] = remote_platform_cores[core]
|
|
241
|
+
|
|
242
|
+
if "device_configs" in remote and dev and dev != core:
|
|
243
|
+
remote_device_configs = remote.get("device_configs", {})
|
|
244
|
+
device_key = device_name_to_path(dev)
|
|
245
|
+
if device_key in remote_device_configs:
|
|
246
|
+
if "device_configs" not in local:
|
|
247
|
+
local["device_configs"] = {}
|
|
248
|
+
local["device_configs"][device_key] = remote_device_configs[device_key]
|
|
249
|
+
|
|
250
|
+
if "schema_version" in remote:
|
|
251
|
+
local["schema_version"] = remote.get("schema_version")
|
|
252
|
+
|
|
253
|
+
def _local_touch_package(pkg_name: str, pkg_meta: dict) -> None:
|
|
254
|
+
local_packages[pkg_name] = pkg_meta.copy()
|
|
255
|
+
|
|
256
|
+
exts = (".py", ".pyi", ".json")
|
|
257
|
+
|
|
258
|
+
def _plan_for(scope: str, target_name: str, part: str) -> list:
|
|
259
|
+
todo = []
|
|
260
|
+
remote_packages = remote.get("packages", {})
|
|
261
|
+
|
|
262
|
+
if scope == "core":
|
|
263
|
+
walker = RegistryHelper.walk_files_for_core(remote, target_name, part)
|
|
264
|
+
else:
|
|
265
|
+
walker = RegistryHelper.walk_files_for_device(remote, target_name, part)
|
|
266
|
+
|
|
267
|
+
for relpath, pkg_meta in walker:
|
|
268
|
+
if not relpath.endswith(exts):
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
source = pkg_meta.get("source", "")
|
|
272
|
+
rver = RegistryHelper.get_version(pkg_meta)
|
|
273
|
+
|
|
274
|
+
orig_pkg_name = None
|
|
275
|
+
orig_pkg_meta = None
|
|
276
|
+
|
|
277
|
+
for name, meta in remote_packages.items():
|
|
278
|
+
if meta.get("source") == source:
|
|
279
|
+
orig_pkg_name = name
|
|
280
|
+
orig_pkg_meta = meta
|
|
281
|
+
break
|
|
282
|
+
variants = meta.get("variants", {})
|
|
283
|
+
for var_name, var_meta in variants.items():
|
|
284
|
+
if var_meta.get("source") == source:
|
|
285
|
+
orig_pkg_name = name
|
|
286
|
+
orig_pkg_meta = meta.copy()
|
|
287
|
+
orig_pkg_meta["source"] = source
|
|
288
|
+
break
|
|
289
|
+
if orig_pkg_name:
|
|
290
|
+
break
|
|
291
|
+
|
|
292
|
+
if not orig_pkg_name or not orig_pkg_meta:
|
|
293
|
+
pkg_name = source.split("/")[-1].replace(".py", "").replace(".pyi", "").replace("__init__", "")
|
|
294
|
+
if not pkg_name or pkg_name == "":
|
|
295
|
+
pkg_name = source.split("/")[-2] if len(source.split("/")) >= 2 else "unknown"
|
|
296
|
+
orig_pkg_name = pkg_name
|
|
297
|
+
orig_pkg_meta = pkg_meta
|
|
298
|
+
|
|
299
|
+
source_for_match = (
|
|
300
|
+
source
|
|
301
|
+
or orig_pkg_meta.get("source", "")
|
|
302
|
+
or orig_pkg_meta.get("typehint", "")
|
|
303
|
+
or ""
|
|
304
|
+
)
|
|
305
|
+
lver, missing = find_local_pkg_version(
|
|
306
|
+
local,
|
|
307
|
+
scope=scope,
|
|
308
|
+
target=target_name,
|
|
309
|
+
source_path=source_for_match,
|
|
310
|
+
pkg_name=orig_pkg_name,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
change_type = "NEW" if missing else ("UPD" if float(lver) < float(rver) else "")
|
|
314
|
+
|
|
315
|
+
if float(lver) < float(rver):
|
|
316
|
+
display_name = source.split("/")[-1].replace(".py", "").replace(".pyi", "")
|
|
317
|
+
todo.append((relpath, pkg_meta, display_name, orig_pkg_name, orig_pkg_meta, change_type))
|
|
318
|
+
|
|
319
|
+
return todo
|
|
320
|
+
|
|
321
|
+
plan = []
|
|
322
|
+
new_count = 0
|
|
323
|
+
upd_count = 0
|
|
324
|
+
download_targets = []
|
|
325
|
+
|
|
326
|
+
def _append_plan(scope: str, target_name: str, part: str, entries: list):
|
|
327
|
+
nonlocal new_count, upd_count
|
|
328
|
+
for relpath, pkg_meta, display_name, orig_pkg_name, orig_pkg_meta, change_type in entries:
|
|
329
|
+
plan.append((scope, target_name, part, relpath, pkg_meta, display_name,
|
|
330
|
+
orig_pkg_name, orig_pkg_meta, change_type))
|
|
331
|
+
if change_type == "NEW":
|
|
332
|
+
new_count += 1
|
|
333
|
+
elif change_type == "UPD":
|
|
334
|
+
upd_count += 1
|
|
335
|
+
|
|
336
|
+
if core_exists_remote and include_core:
|
|
337
|
+
core_src_files = _plan_for("core", core, "src")
|
|
338
|
+
core_hints_files = _plan_for("core", core, "typehints")
|
|
339
|
+
|
|
340
|
+
if file_filter:
|
|
341
|
+
core_src_files = [
|
|
342
|
+
item for item in core_src_files
|
|
343
|
+
if os.path.basename(item[0]) == file_filter
|
|
344
|
+
]
|
|
345
|
+
core_hints_files = []
|
|
346
|
+
|
|
347
|
+
_append_plan("core", core, "src", core_src_files)
|
|
348
|
+
_append_plan("core", core, "typehints", core_hints_files)
|
|
349
|
+
|
|
350
|
+
if core_src_files or core_hints_files:
|
|
351
|
+
download_targets.append(f"core/{core}")
|
|
352
|
+
|
|
353
|
+
if device_exists_remote and include_device:
|
|
354
|
+
device_src_files = _plan_for("device", device_name_to_path(dev), "src")
|
|
355
|
+
device_hints_files = _plan_for("device", device_name_to_path(dev), "typehints")
|
|
356
|
+
|
|
357
|
+
if file_filter:
|
|
358
|
+
device_src_files = [
|
|
359
|
+
item for item in device_src_files
|
|
360
|
+
if os.path.basename(item[0]) == file_filter
|
|
361
|
+
]
|
|
362
|
+
device_hints_files = []
|
|
363
|
+
|
|
364
|
+
_append_plan("device", device_name_to_path(dev), "src", device_src_files)
|
|
365
|
+
_append_plan("device", device_name_to_path(dev), "typehints", device_hints_files)
|
|
366
|
+
|
|
367
|
+
if device_src_files or device_hints_files:
|
|
368
|
+
download_targets.append(f"device/{dev}")
|
|
369
|
+
|
|
370
|
+
download_target = " + ".join(download_targets) if download_targets else ""
|
|
371
|
+
|
|
372
|
+
total = len(plan)
|
|
373
|
+
errors: list[str] = []
|
|
374
|
+
|
|
375
|
+
if total > 0:
|
|
376
|
+
def download_file(task):
|
|
377
|
+
scope, target, part, relpath, pkg_meta, display_name, orig_pkg_name, orig_pkg_meta, _change_type = task
|
|
378
|
+
|
|
379
|
+
if part == "typehints":
|
|
380
|
+
source = pkg_meta.get("typehint", "")
|
|
381
|
+
if not source:
|
|
382
|
+
return (False, relpath, "No typehint path in metadata")
|
|
383
|
+
else:
|
|
384
|
+
source = pkg_meta.get("source", "")
|
|
385
|
+
|
|
386
|
+
if scope == "device" and part == "typehints":
|
|
387
|
+
out_path = os.path.join(StoreManager.pkg_root(), scope, target, part, target, relpath.replace("/", os.sep))
|
|
388
|
+
else:
|
|
389
|
+
out_path = os.path.join(StoreManager.pkg_root(), scope, target, part, relpath.replace("/", os.sep))
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
393
|
+
InstallHelper.download_raw_file(owner, repo, ref, source, out_path)
|
|
394
|
+
|
|
395
|
+
if part == "typehints":
|
|
396
|
+
submodules = pkg_meta.get("submodules_typehints", [])
|
|
397
|
+
else:
|
|
398
|
+
submodules = pkg_meta.get("submodules", [])
|
|
399
|
+
|
|
400
|
+
for submodule_path in submodules:
|
|
401
|
+
relpath_dir = os.path.dirname(relpath) if "/" in relpath or "\\" in relpath else ""
|
|
402
|
+
submodule_filename = os.path.basename(submodule_path)
|
|
403
|
+
sub_relpath = os.path.join(relpath_dir, submodule_filename) if relpath_dir else submodule_filename
|
|
404
|
+
|
|
405
|
+
if scope == "device" and part == "typehints":
|
|
406
|
+
sub_out_path = os.path.join(StoreManager.pkg_root(), scope, target, part, target, sub_relpath.replace("/", os.sep))
|
|
407
|
+
else:
|
|
408
|
+
sub_out_path = os.path.join(StoreManager.pkg_root(), scope, target, part, sub_relpath.replace("/", os.sep))
|
|
409
|
+
|
|
410
|
+
os.makedirs(os.path.dirname(sub_out_path), exist_ok=True)
|
|
411
|
+
InstallHelper.download_raw_file(owner, repo, ref, submodule_path, sub_out_path)
|
|
412
|
+
|
|
413
|
+
unique_pkg_name = f"{scope}:{target}:{orig_pkg_name}"
|
|
414
|
+
source_key = source.replace("\\", "/") if source else ""
|
|
415
|
+
if source_key:
|
|
416
|
+
_local_touch_package(f"{unique_pkg_name}@{source_key}", orig_pkg_meta)
|
|
417
|
+
else:
|
|
418
|
+
_local_touch_package(unique_pkg_name, orig_pkg_meta)
|
|
419
|
+
return (True, relpath, None)
|
|
420
|
+
except urllib.error.HTTPError:
|
|
421
|
+
url = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{source}"
|
|
422
|
+
return (False, relpath, f"404 Not Found - URL: {url}")
|
|
423
|
+
except OSError as e:
|
|
424
|
+
return (False, relpath, f"Local: {e}")
|
|
425
|
+
except Exception as e:
|
|
426
|
+
return (False, relpath, str(e))
|
|
427
|
+
|
|
428
|
+
default_workers = 4
|
|
429
|
+
max_workers = min(
|
|
430
|
+
int(os.environ.get("REPLX_DOWNLOAD_THREADS", str(default_workers))),
|
|
431
|
+
total,
|
|
432
|
+
8
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
done = 0
|
|
436
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
437
|
+
future_to_task = {executor.submit(download_file, task): task for task in plan}
|
|
438
|
+
|
|
439
|
+
for future in as_completed(future_to_task):
|
|
440
|
+
success, relpath, error = future.result()
|
|
441
|
+
done += 1
|
|
442
|
+
if not success:
|
|
443
|
+
errors.append(f"{relpath}: {error}")
|
|
444
|
+
if progress:
|
|
445
|
+
progress({
|
|
446
|
+
"phase": "download",
|
|
447
|
+
"current": done,
|
|
448
|
+
"total": total,
|
|
449
|
+
"file": relpath,
|
|
450
|
+
"target": download_target,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
if device_exists_remote and dev:
|
|
454
|
+
device_typehints_pkg_dir = os.path.join(
|
|
455
|
+
StoreManager.pkg_root(), "device", device_name_to_path(dev),
|
|
456
|
+
"typehints", device_name_to_path(dev))
|
|
457
|
+
init_pyi_path = os.path.join(device_typehints_pkg_dir, "__init__.pyi")
|
|
458
|
+
|
|
459
|
+
if os.path.isdir(device_typehints_pkg_dir) and not os.path.exists(init_pyi_path):
|
|
460
|
+
try:
|
|
461
|
+
with open(init_pyi_path, "w", encoding="utf-8") as f:
|
|
462
|
+
f.write("# Type hints for device package\n")
|
|
463
|
+
except Exception:
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
StoreManager.save_local_meta(local)
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
"core": core,
|
|
470
|
+
"device": dev,
|
|
471
|
+
"core_in_registry": True,
|
|
472
|
+
"device_in_registry": device_exists_remote,
|
|
473
|
+
"total": total,
|
|
474
|
+
"new": new_count,
|
|
475
|
+
"upd": upd_count,
|
|
476
|
+
"errors": errors,
|
|
477
|
+
"target": download_target,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def build_install_batch(spec: str, core: str, device: str, version: str,
|
|
482
|
+
progress: Optional[ProgressFn] = None) -> dict:
|
|
483
|
+
if not (spec.startswith("core.") or spec.startswith("device.")):
|
|
484
|
+
raise PkgError(f"Invalid spec format: {spec}")
|
|
485
|
+
|
|
486
|
+
scope, rest = InstallHelper.resolve_spec(spec)
|
|
487
|
+
base, local_list = InstallHelper.list_local_py_targets(scope, rest, core, device)
|
|
488
|
+
if not local_list:
|
|
489
|
+
raise PkgError("No local files to install. Run 'replx pkg download' first.")
|
|
490
|
+
|
|
491
|
+
total = len(local_list)
|
|
492
|
+
arch_tag = CompilerHelper.mpy_arch_tag(core)
|
|
493
|
+
|
|
494
|
+
batch_specs: list[tuple[str, str]] = []
|
|
495
|
+
unique_dirs: set[str] = set()
|
|
496
|
+
|
|
497
|
+
for idx, (abs_file, rel) in enumerate(local_list):
|
|
498
|
+
if progress:
|
|
499
|
+
progress({
|
|
500
|
+
"phase": "compile",
|
|
501
|
+
"current": idx,
|
|
502
|
+
"total": total,
|
|
503
|
+
"file": os.path.basename(rel),
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
rel_dir = os.path.dirname(rel)
|
|
507
|
+
is_python = abs_file.endswith(".py")
|
|
508
|
+
|
|
509
|
+
if rel.startswith("ext/") and "/" in rel[4:]:
|
|
510
|
+
parts = rel.split("/")
|
|
511
|
+
if len(parts) >= 3:
|
|
512
|
+
rel_dir = "ext"
|
|
513
|
+
remote_dir = InstallHelper.remote_dir_for(scope, rel_dir, device)
|
|
514
|
+
|
|
515
|
+
if is_python:
|
|
516
|
+
CompilerHelper.compile_to_staging(abs_file, base, core, version)
|
|
517
|
+
out_file = CompilerHelper.staging_out_for(abs_file, base, arch_tag)
|
|
518
|
+
remote_path = ("/" + remote_dir + os.path.splitext(parts[-1])[0] + ".mpy").replace("//", "/")
|
|
519
|
+
else:
|
|
520
|
+
out_file = abs_file
|
|
521
|
+
remote_path = ("/" + remote_dir + parts[-1]).replace("//", "/")
|
|
522
|
+
|
|
523
|
+
batch_specs.append((out_file, remote_path))
|
|
524
|
+
unique_dirs.add(remote_dir)
|
|
525
|
+
continue
|
|
526
|
+
|
|
527
|
+
remote_dir = InstallHelper.remote_dir_for(scope, rel_dir, device)
|
|
528
|
+
|
|
529
|
+
if is_python:
|
|
530
|
+
CompilerHelper.compile_to_staging(abs_file, base, core, version)
|
|
531
|
+
out_file = CompilerHelper.staging_out_for(abs_file, base, arch_tag)
|
|
532
|
+
remote_path = ("/" + remote_dir + os.path.splitext(os.path.basename(rel))[0] + ".mpy").replace("//", "/")
|
|
533
|
+
else:
|
|
534
|
+
out_file = abs_file
|
|
535
|
+
remote_path = ("/" + remote_dir + os.path.basename(rel)).replace("//", "/")
|
|
536
|
+
|
|
537
|
+
batch_specs.append((out_file, remote_path))
|
|
538
|
+
|
|
539
|
+
if remote_dir:
|
|
540
|
+
unique_dirs.add(remote_dir)
|
|
541
|
+
|
|
542
|
+
all_paths = set()
|
|
543
|
+
for remote_dir in sorted(unique_dirs):
|
|
544
|
+
parts = [p for p in remote_dir.replace("\\", "/").strip("/").split("/") if p]
|
|
545
|
+
path = ""
|
|
546
|
+
for p in parts:
|
|
547
|
+
path = path + "/" + p
|
|
548
|
+
all_paths.add(path)
|
|
549
|
+
|
|
550
|
+
file_sizes = [os.path.getsize(lp) if os.path.exists(lp) else 0 for lp, _ in batch_specs]
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
"specs": batch_specs,
|
|
554
|
+
"sizes": file_sizes,
|
|
555
|
+
"dirs": sorted(all_paths),
|
|
556
|
+
"files": total,
|
|
557
|
+
"total_bytes": sum(file_sizes),
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def clean(core: str, device: str) -> dict:
|
|
562
|
+
pkg_root = StoreManager.pkg_root()
|
|
563
|
+
device_key = device_name_to_path(device)
|
|
564
|
+
core_path = os.path.join(pkg_root, "core", core)
|
|
565
|
+
device_path = os.path.join(pkg_root, "device", device_key)
|
|
566
|
+
meta_path = StoreManager.local_meta_path()
|
|
567
|
+
|
|
568
|
+
core_exists = os.path.isdir(core_path)
|
|
569
|
+
device_exists = os.path.isdir(device_path)
|
|
570
|
+
meta_exists = os.path.isfile(meta_path)
|
|
571
|
+
|
|
572
|
+
if not core_exists and not device_exists:
|
|
573
|
+
return {"removed": [], "total_size": 0, "found": False}
|
|
574
|
+
|
|
575
|
+
def get_dir_size(path: str) -> int:
|
|
576
|
+
total = 0
|
|
577
|
+
for dirpath, _dirnames, filenames in os.walk(path):
|
|
578
|
+
for f in filenames:
|
|
579
|
+
fp = os.path.join(dirpath, f)
|
|
580
|
+
try:
|
|
581
|
+
total += os.path.getsize(fp)
|
|
582
|
+
except OSError:
|
|
583
|
+
pass
|
|
584
|
+
return total
|
|
585
|
+
|
|
586
|
+
removed_items = []
|
|
587
|
+
total_size = 0
|
|
588
|
+
|
|
589
|
+
with StoreManager.LOCK:
|
|
590
|
+
if core_exists:
|
|
591
|
+
size = get_dir_size(core_path)
|
|
592
|
+
shutil.rmtree(core_path)
|
|
593
|
+
removed_items.append({"item": f"core/{core}/", "size": size})
|
|
594
|
+
total_size += size
|
|
595
|
+
|
|
596
|
+
if device_exists:
|
|
597
|
+
size = get_dir_size(device_path)
|
|
598
|
+
shutil.rmtree(device_path)
|
|
599
|
+
removed_items.append({"item": f"device/{device}/", "size": size})
|
|
600
|
+
total_size += size
|
|
601
|
+
|
|
602
|
+
if meta_exists:
|
|
603
|
+
local_meta = StoreManager.load_local_meta()
|
|
604
|
+
local_packages = local_meta.get("packages", {})
|
|
605
|
+
|
|
606
|
+
core_prefix = f"core:{core}:"
|
|
607
|
+
device_prefix = f"device:{device_key}:"
|
|
608
|
+
|
|
609
|
+
keys_to_remove = [k for k in local_packages.keys()
|
|
610
|
+
if k.startswith(core_prefix) or k.startswith(device_prefix)]
|
|
611
|
+
|
|
612
|
+
entries_removed = 0
|
|
613
|
+
if keys_to_remove:
|
|
614
|
+
for key in keys_to_remove:
|
|
615
|
+
del local_packages[key]
|
|
616
|
+
entries_removed += len(keys_to_remove)
|
|
617
|
+
|
|
618
|
+
if core_exists and "platform_cores" in local_meta:
|
|
619
|
+
if core in local_meta["platform_cores"]:
|
|
620
|
+
del local_meta["platform_cores"][core]
|
|
621
|
+
entries_removed += 1
|
|
622
|
+
|
|
623
|
+
if device_exists and "device_configs" in local_meta:
|
|
624
|
+
if device_key in local_meta["device_configs"]:
|
|
625
|
+
del local_meta["device_configs"][device_key]
|
|
626
|
+
entries_removed += 1
|
|
627
|
+
|
|
628
|
+
if entries_removed > 0:
|
|
629
|
+
StoreManager.save_local_meta(local_meta)
|
|
630
|
+
removed_items.append({"item": "registry.json", "entries": entries_removed})
|
|
631
|
+
|
|
632
|
+
return {"removed": removed_items, "total_size": total_size, "found": True}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def store_status(core: str, device: str) -> dict:
|
|
636
|
+
StoreManager.ensure_home_store()
|
|
637
|
+
ready, reason = InstallHelper.local_store_ready_for_full_install(core, device)
|
|
638
|
+
return {
|
|
639
|
+
"ready": ready,
|
|
640
|
+
"reason": reason,
|
|
641
|
+
"meta": os.path.isfile(StoreManager.local_meta_path()),
|
|
642
|
+
"core_src": os.path.isdir(os.path.join(StoreManager.pkg_root(), "core", core, "src")),
|
|
643
|
+
"device_src": os.path.isdir(os.path.join(
|
|
644
|
+
StoreManager.pkg_root(), "device", device_name_to_path(device), "src")),
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _typehints_dir(scope: str, core: str, device: str) -> str:
|
|
649
|
+
if scope == "core":
|
|
650
|
+
return os.path.join(StoreManager.pkg_root(), "core", core, "typehints")
|
|
651
|
+
return os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(device), "typehints")
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def typehints_list(core: str, device: str) -> dict:
|
|
655
|
+
files = []
|
|
656
|
+
for scope in ("core", "device"):
|
|
657
|
+
base = _typehints_dir(scope, core, device)
|
|
658
|
+
if not os.path.isdir(base):
|
|
659
|
+
continue
|
|
660
|
+
for dp, _dirs, fns in os.walk(base):
|
|
661
|
+
for fn in fns:
|
|
662
|
+
ap = os.path.join(dp, fn)
|
|
663
|
+
rel = os.path.relpath(ap, base).replace("\\", "/")
|
|
664
|
+
digest = hashlib.sha256()
|
|
665
|
+
try:
|
|
666
|
+
with open(ap, "rb") as f:
|
|
667
|
+
for chunk in iter(lambda: f.read(65536), b""):
|
|
668
|
+
digest.update(chunk)
|
|
669
|
+
files.append({
|
|
670
|
+
"scope": scope,
|
|
671
|
+
"relpath": rel,
|
|
672
|
+
"size": os.path.getsize(ap),
|
|
673
|
+
"sha256": digest.hexdigest(),
|
|
674
|
+
})
|
|
675
|
+
except OSError:
|
|
676
|
+
continue
|
|
677
|
+
return {"core": core, "device": device, "files": files}
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def typehints_read(scope: str, relpath: str, core: str, device: str) -> bytes:
|
|
681
|
+
if scope not in ("core", "device"):
|
|
682
|
+
raise PkgError(f"Invalid typehints scope: {scope}")
|
|
683
|
+
base = os.path.realpath(_typehints_dir(scope, core, device))
|
|
684
|
+
target = os.path.realpath(os.path.join(base, relpath.replace("/", os.sep)))
|
|
685
|
+
if not (target == base or target.startswith(base + os.sep)):
|
|
686
|
+
raise PkgError(f"Invalid typehints path: {relpath}")
|
|
687
|
+
if not os.path.isfile(target):
|
|
688
|
+
raise PkgError(f"Typehint file not found: {relpath}")
|
|
689
|
+
with open(target, "rb") as f:
|
|
690
|
+
return f.read()
|