fbuild 1.2.8__py3-none-any.whl → 1.2.15__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.
Files changed (47) hide show
  1. fbuild/__init__.py +5 -1
  2. fbuild/build/configurable_compiler.py +49 -6
  3. fbuild/build/configurable_linker.py +14 -9
  4. fbuild/build/orchestrator_esp32.py +6 -3
  5. fbuild/build/orchestrator_rp2040.py +6 -2
  6. fbuild/cli.py +300 -5
  7. fbuild/config/ini_parser.py +13 -1
  8. fbuild/daemon/__init__.py +11 -0
  9. fbuild/daemon/async_client.py +5 -4
  10. fbuild/daemon/async_client_lib.py +1543 -0
  11. fbuild/daemon/async_protocol.py +825 -0
  12. fbuild/daemon/async_server.py +2100 -0
  13. fbuild/daemon/client.py +425 -13
  14. fbuild/daemon/configuration_lock.py +13 -13
  15. fbuild/daemon/connection.py +508 -0
  16. fbuild/daemon/connection_registry.py +579 -0
  17. fbuild/daemon/daemon.py +517 -164
  18. fbuild/daemon/daemon_context.py +72 -1
  19. fbuild/daemon/device_discovery.py +477 -0
  20. fbuild/daemon/device_manager.py +821 -0
  21. fbuild/daemon/error_collector.py +263 -263
  22. fbuild/daemon/file_cache.py +332 -332
  23. fbuild/daemon/firmware_ledger.py +46 -123
  24. fbuild/daemon/lock_manager.py +508 -508
  25. fbuild/daemon/messages.py +431 -0
  26. fbuild/daemon/operation_registry.py +288 -288
  27. fbuild/daemon/processors/build_processor.py +34 -1
  28. fbuild/daemon/processors/deploy_processor.py +1 -3
  29. fbuild/daemon/processors/locking_processor.py +7 -7
  30. fbuild/daemon/request_processor.py +457 -457
  31. fbuild/daemon/shared_serial.py +7 -7
  32. fbuild/daemon/status_manager.py +238 -238
  33. fbuild/daemon/subprocess_manager.py +316 -316
  34. fbuild/deploy/docker_utils.py +182 -2
  35. fbuild/deploy/monitor.py +1 -1
  36. fbuild/deploy/qemu_runner.py +71 -13
  37. fbuild/ledger/board_ledger.py +46 -122
  38. fbuild/output.py +238 -2
  39. fbuild/packages/library_compiler.py +15 -5
  40. fbuild/packages/library_manager.py +12 -6
  41. fbuild-1.2.15.dist-info/METADATA +569 -0
  42. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/RECORD +46 -39
  43. fbuild-1.2.8.dist-info/METADATA +0 -468
  44. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/WHEEL +0 -0
  45. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/entry_points.txt +0 -0
  46. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/licenses/LICENSE +0 -0
  47. {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/top_level.txt +0 -0
@@ -3,17 +3,18 @@ Firmware Ledger - Track deployed firmware on devices.
3
3
 
4
4
  This module provides a ledger to track what firmware is currently deployed on each
5
5
  device/port, allowing clients to skip re-upload if the same firmware is already running.
6
- The cache is stored in ~/.fbuild/firmware_ledger.json (or dev path if FBUILD_DEV_MODE)
7
- and uses file locking for thread-safe access.
6
+ The cache is stored in ~/.fbuild/firmware_ledger.json (or dev path if FBUILD_DEV_MODE).
8
7
 
9
8
  Features:
10
9
  - Port to firmware hash mapping with timestamps
11
10
  - Source file hash tracking for change detection
12
11
  - Build flags hash for build configuration tracking
13
12
  - Automatic stale entry expiration (configurable, default 24 hours)
14
- - Thread-safe file access with file locking
13
+ - Thread-safe in-process access via threading.Lock
15
14
  - Skip re-upload when firmware matches what's deployed
16
15
 
16
+ Note: Cross-process synchronization is handled by the daemon which holds locks in memory.
17
+
17
18
  Example:
18
19
  >>> from fbuild.daemon.firmware_ledger import FirmwareLedger, compute_firmware_hash
19
20
  >>>
@@ -30,7 +31,6 @@ Example:
30
31
  import hashlib
31
32
  import json
32
33
  import os
33
- import sys
34
34
  import threading
35
35
  import time
36
36
  from dataclasses import dataclass
@@ -131,7 +131,8 @@ class FirmwareLedger:
131
131
  """Manages port to firmware mapping with persistent storage.
132
132
 
133
133
  The ledger stores mappings in ~/.fbuild/firmware_ledger.json (or dev path)
134
- and provides thread-safe access through file locking.
134
+ and provides thread-safe in-process access through threading.Lock.
135
+ Cross-process synchronization is handled by the daemon which holds locks in memory.
135
136
 
136
137
  Example:
137
138
  >>> ledger = FirmwareLedger()
@@ -196,60 +197,6 @@ class FirmwareLedger:
196
197
  except OSError as e:
197
198
  raise FirmwareLedgerError(f"Failed to write ledger: {e}") from e
198
199
 
199
- def _acquire_file_lock(self) -> Any:
200
- """Acquire a file lock for cross-process synchronization.
201
-
202
- Returns:
203
- Lock file handle (or None on platforms without locking support)
204
- """
205
- self._ensure_directory()
206
- lock_path = self._ledger_path.with_suffix(".lock")
207
-
208
- try:
209
- # Open lock file
210
- lock_file = open(lock_path, "w", encoding="utf-8")
211
-
212
- # Platform-specific locking
213
- if sys.platform == "win32":
214
- import msvcrt
215
-
216
- msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
217
- else: # pragma: no cover - Unix only
218
- import fcntl # type: ignore[import-not-found]
219
-
220
- fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
221
-
222
- return lock_file
223
- except (ImportError, OSError):
224
- # Locking not available or failed - continue without lock
225
- return None
226
-
227
- def _release_file_lock(self, lock_file: Any) -> None:
228
- """Release a file lock.
229
-
230
- Args:
231
- lock_file: Lock file handle from _acquire_file_lock
232
- """
233
- if lock_file is None:
234
- return
235
-
236
- try:
237
- if sys.platform == "win32":
238
- import msvcrt
239
-
240
- try:
241
- msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
242
- except OSError:
243
- pass
244
- else: # pragma: no cover - Unix only
245
- import fcntl # type: ignore[import-not-found]
246
-
247
- fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
248
-
249
- lock_file.close()
250
- except (ImportError, OSError):
251
- pass
252
-
253
200
  def record_deployment(
254
201
  self,
255
202
  port: str,
@@ -280,13 +227,9 @@ class FirmwareLedger:
280
227
  )
281
228
 
282
229
  with self._lock:
283
- lock_file = self._acquire_file_lock()
284
- try:
285
- data = self._read_ledger()
286
- data[port] = entry.to_dict()
287
- self._write_ledger(data)
288
- finally:
289
- self._release_file_lock(lock_file)
230
+ data = self._read_ledger()
231
+ data[port] = entry.to_dict()
232
+ self._write_ledger(data)
290
233
 
291
234
  def get_deployment(self, port: str) -> FirmwareEntry | None:
292
235
  """Get the deployment entry for a port.
@@ -298,20 +241,16 @@ class FirmwareLedger:
298
241
  FirmwareEntry or None if not found or stale
299
242
  """
300
243
  with self._lock:
301
- lock_file = self._acquire_file_lock()
302
- try:
303
- data = self._read_ledger()
304
- entry_data = data.get(port)
305
- if entry_data is None:
306
- return None
244
+ data = self._read_ledger()
245
+ entry_data = data.get(port)
246
+ if entry_data is None:
247
+ return None
307
248
 
308
- entry = FirmwareEntry.from_dict(entry_data)
309
- if entry.is_stale():
310
- return None
249
+ entry = FirmwareEntry.from_dict(entry_data)
250
+ if entry.is_stale():
251
+ return None
311
252
 
312
- return entry
313
- finally:
314
- self._release_file_lock(lock_file)
253
+ return entry
315
254
 
316
255
  def is_current(
317
256
  self,
@@ -384,16 +323,12 @@ class FirmwareLedger:
384
323
  True if entry was cleared, False if not found
385
324
  """
386
325
  with self._lock:
387
- lock_file = self._acquire_file_lock()
388
- try:
389
- data = self._read_ledger()
390
- if port in data:
391
- del data[port]
392
- self._write_ledger(data)
393
- return True
394
- return False
395
- finally:
396
- self._release_file_lock(lock_file)
326
+ data = self._read_ledger()
327
+ if port in data:
328
+ del data[port]
329
+ self._write_ledger(data)
330
+ return True
331
+ return False
397
332
 
398
333
  def clear_all(self) -> int:
399
334
  """Clear all entries from the ledger.
@@ -402,14 +337,10 @@ class FirmwareLedger:
402
337
  Number of entries cleared
403
338
  """
404
339
  with self._lock:
405
- lock_file = self._acquire_file_lock()
406
- try:
407
- data = self._read_ledger()
408
- count = len(data)
409
- self._write_ledger({})
410
- return count
411
- finally:
412
- self._release_file_lock(lock_file)
340
+ data = self._read_ledger()
341
+ count = len(data)
342
+ self._write_ledger({})
343
+ return count
413
344
 
414
345
  def clear_stale(
415
346
  self,
@@ -424,22 +355,18 @@ class FirmwareLedger:
424
355
  Number of entries removed
425
356
  """
426
357
  with self._lock:
427
- lock_file = self._acquire_file_lock()
428
- try:
429
- data = self._read_ledger()
430
- original_count = len(data)
431
-
432
- # Filter out stale entries
433
- fresh_data = {}
434
- for port, entry_data in data.items():
435
- entry = FirmwareEntry.from_dict(entry_data)
436
- if not entry.is_stale(threshold_seconds):
437
- fresh_data[port] = entry_data
438
-
439
- self._write_ledger(fresh_data)
440
- return original_count - len(fresh_data)
441
- finally:
442
- self._release_file_lock(lock_file)
358
+ data = self._read_ledger()
359
+ original_count = len(data)
360
+
361
+ # Filter out stale entries
362
+ fresh_data = {}
363
+ for port, entry_data in data.items():
364
+ entry = FirmwareEntry.from_dict(entry_data)
365
+ if not entry.is_stale(threshold_seconds):
366
+ fresh_data[port] = entry_data
367
+
368
+ self._write_ledger(fresh_data)
369
+ return original_count - len(fresh_data)
443
370
 
444
371
  def get_all(self) -> dict[str, FirmwareEntry]:
445
372
  """Get all non-stale entries in the ledger.
@@ -448,17 +375,13 @@ class FirmwareLedger:
448
375
  Dictionary mapping port names to FirmwareEntry objects
449
376
  """
450
377
  with self._lock:
451
- lock_file = self._acquire_file_lock()
452
- try:
453
- data = self._read_ledger()
454
- result = {}
455
- for port, entry_data in data.items():
456
- entry = FirmwareEntry.from_dict(entry_data)
457
- if not entry.is_stale():
458
- result[port] = entry
459
- return result
460
- finally:
461
- self._release_file_lock(lock_file)
378
+ data = self._read_ledger()
379
+ result = {}
380
+ for port, entry_data in data.items():
381
+ entry = FirmwareEntry.from_dict(entry_data)
382
+ if not entry.is_stale():
383
+ result[port] = entry
384
+ return result
462
385
 
463
386
 
464
387
  def compute_firmware_hash(firmware_path: Path) -> str: