fbuild 1.2.8__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.
- fbuild/__init__.py +390 -0
- fbuild/assets/example.txt +1 -0
- fbuild/build/__init__.py +117 -0
- fbuild/build/archive_creator.py +186 -0
- fbuild/build/binary_generator.py +444 -0
- fbuild/build/build_component_factory.py +131 -0
- fbuild/build/build_info_generator.py +624 -0
- fbuild/build/build_state.py +325 -0
- fbuild/build/build_utils.py +93 -0
- fbuild/build/compilation_executor.py +422 -0
- fbuild/build/compiler.py +165 -0
- fbuild/build/compiler_avr.py +574 -0
- fbuild/build/configurable_compiler.py +664 -0
- fbuild/build/configurable_linker.py +637 -0
- fbuild/build/flag_builder.py +214 -0
- fbuild/build/library_dependency_processor.py +185 -0
- fbuild/build/linker.py +708 -0
- fbuild/build/orchestrator.py +67 -0
- fbuild/build/orchestrator_avr.py +651 -0
- fbuild/build/orchestrator_esp32.py +878 -0
- fbuild/build/orchestrator_rp2040.py +719 -0
- fbuild/build/orchestrator_stm32.py +696 -0
- fbuild/build/orchestrator_teensy.py +580 -0
- fbuild/build/source_compilation_orchestrator.py +218 -0
- fbuild/build/source_scanner.py +516 -0
- fbuild/cli.py +717 -0
- fbuild/cli_utils.py +314 -0
- fbuild/config/__init__.py +16 -0
- fbuild/config/board_config.py +542 -0
- fbuild/config/board_loader.py +92 -0
- fbuild/config/ini_parser.py +369 -0
- fbuild/config/mcu_specs.py +88 -0
- fbuild/daemon/__init__.py +42 -0
- fbuild/daemon/async_client.py +531 -0
- fbuild/daemon/client.py +1505 -0
- fbuild/daemon/compilation_queue.py +293 -0
- fbuild/daemon/configuration_lock.py +865 -0
- fbuild/daemon/daemon.py +585 -0
- fbuild/daemon/daemon_context.py +293 -0
- fbuild/daemon/error_collector.py +263 -0
- fbuild/daemon/file_cache.py +332 -0
- fbuild/daemon/firmware_ledger.py +546 -0
- fbuild/daemon/lock_manager.py +508 -0
- fbuild/daemon/logging_utils.py +149 -0
- fbuild/daemon/messages.py +957 -0
- fbuild/daemon/operation_registry.py +288 -0
- fbuild/daemon/port_state_manager.py +249 -0
- fbuild/daemon/process_tracker.py +366 -0
- fbuild/daemon/processors/__init__.py +18 -0
- fbuild/daemon/processors/build_processor.py +248 -0
- fbuild/daemon/processors/deploy_processor.py +664 -0
- fbuild/daemon/processors/install_deps_processor.py +431 -0
- fbuild/daemon/processors/locking_processor.py +777 -0
- fbuild/daemon/processors/monitor_processor.py +285 -0
- fbuild/daemon/request_processor.py +457 -0
- fbuild/daemon/shared_serial.py +819 -0
- fbuild/daemon/status_manager.py +238 -0
- fbuild/daemon/subprocess_manager.py +316 -0
- fbuild/deploy/__init__.py +21 -0
- fbuild/deploy/deployer.py +67 -0
- fbuild/deploy/deployer_esp32.py +310 -0
- fbuild/deploy/docker_utils.py +315 -0
- fbuild/deploy/monitor.py +519 -0
- fbuild/deploy/qemu_runner.py +603 -0
- fbuild/interrupt_utils.py +34 -0
- fbuild/ledger/__init__.py +52 -0
- fbuild/ledger/board_ledger.py +560 -0
- fbuild/output.py +352 -0
- fbuild/packages/__init__.py +66 -0
- fbuild/packages/archive_utils.py +1098 -0
- fbuild/packages/arduino_core.py +412 -0
- fbuild/packages/cache.py +256 -0
- fbuild/packages/concurrent_manager.py +510 -0
- fbuild/packages/downloader.py +518 -0
- fbuild/packages/fingerprint.py +423 -0
- fbuild/packages/framework_esp32.py +538 -0
- fbuild/packages/framework_rp2040.py +349 -0
- fbuild/packages/framework_stm32.py +459 -0
- fbuild/packages/framework_teensy.py +346 -0
- fbuild/packages/github_utils.py +96 -0
- fbuild/packages/header_trampoline_cache.py +394 -0
- fbuild/packages/library_compiler.py +203 -0
- fbuild/packages/library_manager.py +549 -0
- fbuild/packages/library_manager_esp32.py +725 -0
- fbuild/packages/package.py +163 -0
- fbuild/packages/platform_esp32.py +383 -0
- fbuild/packages/platform_rp2040.py +400 -0
- fbuild/packages/platform_stm32.py +581 -0
- fbuild/packages/platform_teensy.py +312 -0
- fbuild/packages/platform_utils.py +131 -0
- fbuild/packages/platformio_registry.py +369 -0
- fbuild/packages/sdk_utils.py +231 -0
- fbuild/packages/toolchain.py +436 -0
- fbuild/packages/toolchain_binaries.py +196 -0
- fbuild/packages/toolchain_esp32.py +489 -0
- fbuild/packages/toolchain_metadata.py +185 -0
- fbuild/packages/toolchain_rp2040.py +436 -0
- fbuild/packages/toolchain_stm32.py +417 -0
- fbuild/packages/toolchain_teensy.py +404 -0
- fbuild/platform_configs/esp32.json +150 -0
- fbuild/platform_configs/esp32c2.json +144 -0
- fbuild/platform_configs/esp32c3.json +143 -0
- fbuild/platform_configs/esp32c5.json +151 -0
- fbuild/platform_configs/esp32c6.json +151 -0
- fbuild/platform_configs/esp32p4.json +149 -0
- fbuild/platform_configs/esp32s3.json +151 -0
- fbuild/platform_configs/imxrt1062.json +56 -0
- fbuild/platform_configs/rp2040.json +70 -0
- fbuild/platform_configs/rp2350.json +76 -0
- fbuild/platform_configs/stm32f1.json +59 -0
- fbuild/platform_configs/stm32f4.json +63 -0
- fbuild/py.typed +0 -0
- fbuild-1.2.8.dist-info/METADATA +468 -0
- fbuild-1.2.8.dist-info/RECORD +121 -0
- fbuild-1.2.8.dist-info/WHEEL +5 -0
- fbuild-1.2.8.dist-info/entry_points.txt +5 -0
- fbuild-1.2.8.dist-info/licenses/LICENSE +21 -0
- fbuild-1.2.8.dist-info/top_level.txt +2 -0
- fbuild_lint/__init__.py +0 -0
- fbuild_lint/ruff_plugins/__init__.py +0 -0
- fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Package fingerprinting for artifact validation and persistence.
|
|
2
|
+
|
|
3
|
+
This module provides fingerprinting capabilities for downloaded packages,
|
|
4
|
+
enabling validation of installation state and detection of corruption or
|
|
5
|
+
incomplete installations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PackageFingerprint:
|
|
18
|
+
"""Fingerprint for validating package installation state.
|
|
19
|
+
|
|
20
|
+
A fingerprint captures the identity and state of a downloaded and
|
|
21
|
+
extracted package, enabling:
|
|
22
|
+
- Verification that an installation matches expected state
|
|
23
|
+
- Detection of corruption or incomplete extraction
|
|
24
|
+
- Cache validation without re-downloading
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
url: str
|
|
28
|
+
version: str
|
|
29
|
+
url_hash: str # SHA256[:16] of URL (for cache key)
|
|
30
|
+
content_hash: str # SHA256 of downloaded archive
|
|
31
|
+
extracted_files: List[str] = field(default_factory=list) # Key files to verify
|
|
32
|
+
install_timestamp: float = field(default_factory=time.time)
|
|
33
|
+
file_count: int = 0
|
|
34
|
+
total_size: int = 0
|
|
35
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
FINGERPRINT_FILENAME = ".fbuild_fingerprint.json"
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def hash_url(url: str) -> str:
|
|
41
|
+
"""Generate a SHA256 hash of a URL for cache directory naming.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
url: The base URL to hash
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
First 16 characters of SHA256 hash (sufficient for uniqueness)
|
|
48
|
+
"""
|
|
49
|
+
return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def hash_file(file_path: Path, chunk_size: int = 8192) -> str:
|
|
53
|
+
"""Generate SHA256 hash of a file.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
file_path: Path to file to hash
|
|
57
|
+
chunk_size: Size of chunks for reading
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
SHA256 hex digest
|
|
61
|
+
"""
|
|
62
|
+
sha256 = hashlib.sha256()
|
|
63
|
+
with open(file_path, "rb") as f:
|
|
64
|
+
for chunk in iter(lambda: f.read(chunk_size), b""):
|
|
65
|
+
sha256.update(chunk)
|
|
66
|
+
return sha256.hexdigest()
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_archive(
|
|
70
|
+
cls,
|
|
71
|
+
url: str,
|
|
72
|
+
version: str,
|
|
73
|
+
archive_path: Path,
|
|
74
|
+
extracted_dir: Optional[Path] = None,
|
|
75
|
+
key_files: Optional[List[str]] = None,
|
|
76
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
77
|
+
) -> "PackageFingerprint":
|
|
78
|
+
"""Create fingerprint from a downloaded archive.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
url: Original download URL
|
|
82
|
+
version: Package version string
|
|
83
|
+
archive_path: Path to downloaded archive
|
|
84
|
+
extracted_dir: Optional path to extracted directory for file enumeration
|
|
85
|
+
key_files: Optional list of key file paths to track (relative to extracted_dir)
|
|
86
|
+
metadata: Optional additional metadata to store
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
PackageFingerprint instance
|
|
90
|
+
"""
|
|
91
|
+
url_hash = cls.hash_url(url)
|
|
92
|
+
content_hash = cls.hash_file(archive_path)
|
|
93
|
+
|
|
94
|
+
extracted_files: List[str] = []
|
|
95
|
+
file_count = 0
|
|
96
|
+
total_size = 0
|
|
97
|
+
|
|
98
|
+
if extracted_dir and extracted_dir.exists():
|
|
99
|
+
# Enumerate files in extracted directory
|
|
100
|
+
for file_path in extracted_dir.rglob("*"):
|
|
101
|
+
if file_path.is_file():
|
|
102
|
+
file_count += 1
|
|
103
|
+
total_size += file_path.stat().st_size
|
|
104
|
+
|
|
105
|
+
# Track key files for quick validation
|
|
106
|
+
if key_files:
|
|
107
|
+
for key_file in key_files:
|
|
108
|
+
full_path = extracted_dir / key_file
|
|
109
|
+
if full_path.exists():
|
|
110
|
+
extracted_files.append(key_file)
|
|
111
|
+
|
|
112
|
+
return cls(
|
|
113
|
+
url=url,
|
|
114
|
+
version=version,
|
|
115
|
+
url_hash=url_hash,
|
|
116
|
+
content_hash=content_hash,
|
|
117
|
+
extracted_files=extracted_files,
|
|
118
|
+
install_timestamp=time.time(),
|
|
119
|
+
file_count=file_count,
|
|
120
|
+
total_size=total_size,
|
|
121
|
+
metadata=metadata or {},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
125
|
+
"""Convert fingerprint to dictionary for JSON serialization."""
|
|
126
|
+
return {
|
|
127
|
+
"url": self.url,
|
|
128
|
+
"version": self.version,
|
|
129
|
+
"url_hash": self.url_hash,
|
|
130
|
+
"content_hash": self.content_hash,
|
|
131
|
+
"extracted_files": self.extracted_files,
|
|
132
|
+
"install_timestamp": self.install_timestamp,
|
|
133
|
+
"file_count": self.file_count,
|
|
134
|
+
"total_size": self.total_size,
|
|
135
|
+
"metadata": self.metadata,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PackageFingerprint":
|
|
140
|
+
"""Create fingerprint from dictionary."""
|
|
141
|
+
return cls(
|
|
142
|
+
url=data["url"],
|
|
143
|
+
version=data["version"],
|
|
144
|
+
url_hash=data["url_hash"],
|
|
145
|
+
content_hash=data["content_hash"],
|
|
146
|
+
extracted_files=data.get("extracted_files", []),
|
|
147
|
+
install_timestamp=data.get("install_timestamp", 0),
|
|
148
|
+
file_count=data.get("file_count", 0),
|
|
149
|
+
total_size=data.get("total_size", 0),
|
|
150
|
+
metadata=data.get("metadata", {}),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def save(self, directory: Path) -> Path:
|
|
154
|
+
"""Save fingerprint to directory.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
directory: Directory to save fingerprint file
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Path to saved fingerprint file
|
|
161
|
+
"""
|
|
162
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
fingerprint_path = directory / self.FINGERPRINT_FILENAME
|
|
164
|
+
|
|
165
|
+
with open(fingerprint_path, "w", encoding="utf-8") as f:
|
|
166
|
+
json.dump(self.to_dict(), f, indent=2)
|
|
167
|
+
|
|
168
|
+
return fingerprint_path
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def load(cls, directory: Path) -> Optional["PackageFingerprint"]:
|
|
172
|
+
"""Load fingerprint from directory.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
directory: Directory containing fingerprint file
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
PackageFingerprint instance or None if not found/invalid
|
|
179
|
+
"""
|
|
180
|
+
fingerprint_path = directory / cls.FINGERPRINT_FILENAME
|
|
181
|
+
|
|
182
|
+
if not fingerprint_path.exists():
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
with open(fingerprint_path, "r", encoding="utf-8") as f:
|
|
187
|
+
data = json.load(f)
|
|
188
|
+
return cls.from_dict(data)
|
|
189
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def matches(self, other: "PackageFingerprint") -> bool:
|
|
193
|
+
"""Check if fingerprints match (same content).
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
other: Another fingerprint to compare
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if fingerprints represent the same package installation
|
|
200
|
+
"""
|
|
201
|
+
return self.url == other.url and self.version == other.version and self.content_hash == other.content_hash
|
|
202
|
+
|
|
203
|
+
def validate_installation(self, directory: Path) -> tuple[bool, str]:
|
|
204
|
+
"""Validate that installation matches fingerprint.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
directory: Directory containing extracted package
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Tuple of (is_valid, reason)
|
|
211
|
+
"""
|
|
212
|
+
if not directory.exists():
|
|
213
|
+
return False, "Directory does not exist"
|
|
214
|
+
|
|
215
|
+
# Check key files exist
|
|
216
|
+
for key_file in self.extracted_files:
|
|
217
|
+
full_path = directory / key_file
|
|
218
|
+
if not full_path.exists():
|
|
219
|
+
return False, f"Missing key file: {key_file}"
|
|
220
|
+
|
|
221
|
+
# Quick file count check (allows for some variance due to temp files)
|
|
222
|
+
if self.file_count > 0:
|
|
223
|
+
actual_count = sum(1 for _ in directory.rglob("*") if _.is_file())
|
|
224
|
+
# Allow 10% variance in file count
|
|
225
|
+
if actual_count < self.file_count * 0.9:
|
|
226
|
+
return False, f"File count mismatch: expected ~{self.file_count}, found {actual_count}"
|
|
227
|
+
|
|
228
|
+
return True, "Installation valid"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class FingerprintRegistry:
|
|
232
|
+
"""Registry for managing package fingerprints across a cache.
|
|
233
|
+
|
|
234
|
+
Provides a centralized way to track all installed packages and
|
|
235
|
+
their fingerprints.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
REGISTRY_FILENAME = ".fbuild_package_registry.json"
|
|
239
|
+
|
|
240
|
+
def __init__(self, cache_root: Path):
|
|
241
|
+
"""Initialize fingerprint registry.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
cache_root: Root directory of the cache
|
|
245
|
+
"""
|
|
246
|
+
self.cache_root = cache_root
|
|
247
|
+
self.registry_path = cache_root / self.REGISTRY_FILENAME
|
|
248
|
+
self._registry: Dict[str, Dict[str, Any]] = {}
|
|
249
|
+
self._load()
|
|
250
|
+
|
|
251
|
+
def _load(self) -> None:
|
|
252
|
+
"""Load registry from disk."""
|
|
253
|
+
if self.registry_path.exists():
|
|
254
|
+
try:
|
|
255
|
+
with open(self.registry_path, "r", encoding="utf-8") as f:
|
|
256
|
+
self._registry = json.load(f)
|
|
257
|
+
except (json.JSONDecodeError, IOError):
|
|
258
|
+
self._registry = {}
|
|
259
|
+
|
|
260
|
+
def _save(self) -> None:
|
|
261
|
+
"""Save registry to disk."""
|
|
262
|
+
self.cache_root.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
with open(self.registry_path, "w", encoding="utf-8") as f:
|
|
264
|
+
json.dump(self._registry, f, indent=2)
|
|
265
|
+
|
|
266
|
+
def register(self, fingerprint: PackageFingerprint, install_path: Path) -> None:
|
|
267
|
+
"""Register a package installation.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
fingerprint: Package fingerprint
|
|
271
|
+
install_path: Path where package is installed
|
|
272
|
+
"""
|
|
273
|
+
key = f"{fingerprint.url_hash}:{fingerprint.version}"
|
|
274
|
+
self._registry[key] = {
|
|
275
|
+
"fingerprint": fingerprint.to_dict(),
|
|
276
|
+
"install_path": str(install_path),
|
|
277
|
+
"registered_at": time.time(),
|
|
278
|
+
}
|
|
279
|
+
self._save()
|
|
280
|
+
|
|
281
|
+
def get_fingerprint(self, url: str, version: str) -> Optional[PackageFingerprint]:
|
|
282
|
+
"""Get fingerprint for a package.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
url: Package URL
|
|
286
|
+
version: Package version
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
PackageFingerprint or None if not registered
|
|
290
|
+
"""
|
|
291
|
+
url_hash = PackageFingerprint.hash_url(url)
|
|
292
|
+
key = f"{url_hash}:{version}"
|
|
293
|
+
|
|
294
|
+
if key not in self._registry:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
return PackageFingerprint.from_dict(self._registry[key]["fingerprint"])
|
|
299
|
+
except (KeyError, TypeError):
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
def get_install_path(self, url: str, version: str) -> Optional[Path]:
|
|
303
|
+
"""Get installation path for a package.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
url: Package URL
|
|
307
|
+
version: Package version
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Installation path or None if not registered
|
|
311
|
+
"""
|
|
312
|
+
url_hash = PackageFingerprint.hash_url(url)
|
|
313
|
+
key = f"{url_hash}:{version}"
|
|
314
|
+
|
|
315
|
+
if key not in self._registry:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
return Path(self._registry[key]["install_path"])
|
|
320
|
+
except (KeyError, TypeError):
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
def is_installed(self, url: str, version: str) -> bool:
|
|
324
|
+
"""Check if a package is installed and valid.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
url: Package URL
|
|
328
|
+
version: Package version
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
True if package is installed and fingerprint is valid
|
|
332
|
+
"""
|
|
333
|
+
fingerprint = self.get_fingerprint(url, version)
|
|
334
|
+
if fingerprint is None:
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
install_path = self.get_install_path(url, version)
|
|
338
|
+
if install_path is None or not install_path.exists():
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
is_valid, _ = fingerprint.validate_installation(install_path)
|
|
342
|
+
return is_valid
|
|
343
|
+
|
|
344
|
+
def unregister(self, url: str, version: str) -> bool:
|
|
345
|
+
"""Unregister a package.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
url: Package URL
|
|
349
|
+
version: Package version
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
True if package was registered and removed
|
|
353
|
+
"""
|
|
354
|
+
url_hash = PackageFingerprint.hash_url(url)
|
|
355
|
+
key = f"{url_hash}:{version}"
|
|
356
|
+
|
|
357
|
+
if key in self._registry:
|
|
358
|
+
del self._registry[key]
|
|
359
|
+
self._save()
|
|
360
|
+
return True
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
def list_packages(self) -> List[Dict[str, Any]]:
|
|
364
|
+
"""List all registered packages.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
List of package information dictionaries
|
|
368
|
+
"""
|
|
369
|
+
packages = []
|
|
370
|
+
for _key, entry in self._registry.items():
|
|
371
|
+
try:
|
|
372
|
+
fingerprint = PackageFingerprint.from_dict(entry["fingerprint"])
|
|
373
|
+
install_path = Path(entry["install_path"])
|
|
374
|
+
is_valid, reason = fingerprint.validate_installation(install_path)
|
|
375
|
+
|
|
376
|
+
packages.append(
|
|
377
|
+
{
|
|
378
|
+
"url": fingerprint.url,
|
|
379
|
+
"version": fingerprint.version,
|
|
380
|
+
"url_hash": fingerprint.url_hash,
|
|
381
|
+
"install_path": str(install_path),
|
|
382
|
+
"is_valid": is_valid,
|
|
383
|
+
"validation_reason": reason,
|
|
384
|
+
"file_count": fingerprint.file_count,
|
|
385
|
+
"total_size": fingerprint.total_size,
|
|
386
|
+
"install_timestamp": fingerprint.install_timestamp,
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
except (KeyError, TypeError):
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
return packages
|
|
393
|
+
|
|
394
|
+
def cleanup_invalid(self) -> int:
|
|
395
|
+
"""Remove entries for invalid/missing installations.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Number of entries removed
|
|
399
|
+
"""
|
|
400
|
+
keys_to_remove = []
|
|
401
|
+
|
|
402
|
+
for key, entry in self._registry.items():
|
|
403
|
+
try:
|
|
404
|
+
fingerprint = PackageFingerprint.from_dict(entry["fingerprint"])
|
|
405
|
+
install_path = Path(entry["install_path"])
|
|
406
|
+
|
|
407
|
+
if not install_path.exists():
|
|
408
|
+
keys_to_remove.append(key)
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
is_valid, _ = fingerprint.validate_installation(install_path)
|
|
412
|
+
if not is_valid:
|
|
413
|
+
keys_to_remove.append(key)
|
|
414
|
+
except (KeyError, TypeError):
|
|
415
|
+
keys_to_remove.append(key)
|
|
416
|
+
|
|
417
|
+
for key in keys_to_remove:
|
|
418
|
+
del self._registry[key]
|
|
419
|
+
|
|
420
|
+
if keys_to_remove:
|
|
421
|
+
self._save()
|
|
422
|
+
|
|
423
|
+
return len(keys_to_remove)
|