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
fbuild/daemon/messages.py CHANGED
@@ -955,3 +955,434 @@ class ClientResponse:
955
955
  total_clients=data.get("total_clients", 0),
956
956
  timestamp=data.get("timestamp", time.time()),
957
957
  )
958
+
959
+
960
+ @dataclass
961
+ class DaemonIdentity:
962
+ """Identity information for a daemon instance.
963
+
964
+ This is returned when clients query daemon identity and is used
965
+ to distinguish between different daemon instances (e.g., dev vs prod).
966
+
967
+ Attributes:
968
+ name: Daemon name (e.g., "fbuild_daemon" or "fbuild_daemon_dev")
969
+ version: Daemon version string
970
+ started_at: Unix timestamp when daemon started
971
+ spawned_by_pid: PID of client that originally started the daemon
972
+ spawned_by_cwd: Working directory of client that started daemon
973
+ is_dev: Whether this is a development mode daemon
974
+ pid: Process ID of the daemon itself
975
+ """
976
+
977
+ name: str
978
+ version: str
979
+ started_at: float
980
+ spawned_by_pid: int
981
+ spawned_by_cwd: str
982
+ is_dev: bool
983
+ pid: int
984
+
985
+ def to_dict(self) -> dict[str, Any]:
986
+ """Convert to dictionary for JSON serialization."""
987
+ return asdict(self)
988
+
989
+ @classmethod
990
+ def from_dict(cls, data: dict[str, Any]) -> "DaemonIdentity":
991
+ """Create DaemonIdentity from dictionary."""
992
+ return cls(
993
+ name=data["name"],
994
+ version=data["version"],
995
+ started_at=data["started_at"],
996
+ spawned_by_pid=data["spawned_by_pid"],
997
+ spawned_by_cwd=data["spawned_by_cwd"],
998
+ is_dev=data["is_dev"],
999
+ pid=data["pid"],
1000
+ )
1001
+
1002
+
1003
+ @dataclass
1004
+ class DaemonIdentityRequest:
1005
+ """Client -> Daemon: Request daemon identity information.
1006
+
1007
+ Attributes:
1008
+ timestamp: Unix timestamp when request was created
1009
+ """
1010
+
1011
+ timestamp: float = field(default_factory=time.time)
1012
+
1013
+ def to_dict(self) -> dict[str, Any]:
1014
+ """Convert to dictionary for JSON serialization."""
1015
+ return asdict(self)
1016
+
1017
+ @classmethod
1018
+ def from_dict(cls, data: dict[str, Any]) -> "DaemonIdentityRequest":
1019
+ """Create DaemonIdentityRequest from dictionary."""
1020
+ return cls(
1021
+ timestamp=data.get("timestamp", time.time()),
1022
+ )
1023
+
1024
+
1025
+ @dataclass
1026
+ class DaemonIdentityResponse:
1027
+ """Daemon -> Client: Response with daemon identity.
1028
+
1029
+ Attributes:
1030
+ success: Whether the request succeeded
1031
+ message: Human-readable message
1032
+ identity: The daemon identity (if success)
1033
+ timestamp: Unix timestamp of response
1034
+ """
1035
+
1036
+ success: bool
1037
+ message: str
1038
+ identity: DaemonIdentity | None = None
1039
+ timestamp: float = field(default_factory=time.time)
1040
+
1041
+ def to_dict(self) -> dict[str, Any]:
1042
+ """Convert to dictionary for JSON serialization."""
1043
+ result = asdict(self)
1044
+ if self.identity:
1045
+ result["identity"] = self.identity.to_dict()
1046
+ return result
1047
+
1048
+ @classmethod
1049
+ def from_dict(cls, data: dict[str, Any]) -> "DaemonIdentityResponse":
1050
+ """Create DaemonIdentityResponse from dictionary."""
1051
+ identity = None
1052
+ if data.get("identity"):
1053
+ identity = DaemonIdentity.from_dict(data["identity"])
1054
+ return cls(
1055
+ success=data["success"],
1056
+ message=data["message"],
1057
+ identity=identity,
1058
+ timestamp=data.get("timestamp", time.time()),
1059
+ )
1060
+
1061
+
1062
+ # =============================================================================
1063
+ # Device Management Messages (Resource Manager Expansion)
1064
+ # =============================================================================
1065
+
1066
+
1067
+ class DeviceLeaseType(Enum):
1068
+ """Type of device lease."""
1069
+
1070
+ EXCLUSIVE = "exclusive" # For deploy/flash/reset (single holder)
1071
+ MONITOR = "monitor" # For read-only monitoring (multiple holders)
1072
+
1073
+
1074
+ @dataclass
1075
+ class DeviceLeaseRequest:
1076
+ """Client -> Daemon: Request to acquire a device lease.
1077
+
1078
+ Used to acquire either exclusive access (for deploy/flash) or
1079
+ monitor access (for read-only serial monitoring).
1080
+
1081
+ Attributes:
1082
+ device_id: The stable device ID to lease
1083
+ lease_type: Type of lease ("exclusive" or "monitor")
1084
+ description: Human-readable description of the operation
1085
+ allows_monitors: For exclusive leases, whether monitors are allowed (default True)
1086
+ timeout: Maximum time in seconds to wait for the lease
1087
+ timestamp: Unix timestamp when request was created
1088
+ """
1089
+
1090
+ device_id: str
1091
+ lease_type: str # "exclusive" or "monitor"
1092
+ description: str = ""
1093
+ allows_monitors: bool = True
1094
+ timeout: float = 300.0
1095
+ timestamp: float = field(default_factory=time.time)
1096
+
1097
+ def to_dict(self) -> dict[str, Any]:
1098
+ """Convert to dictionary for JSON serialization."""
1099
+ return asdict(self)
1100
+
1101
+ @classmethod
1102
+ def from_dict(cls, data: dict[str, Any]) -> "DeviceLeaseRequest":
1103
+ """Create DeviceLeaseRequest from dictionary."""
1104
+ return cls(
1105
+ device_id=data["device_id"],
1106
+ lease_type=data["lease_type"],
1107
+ description=data.get("description", ""),
1108
+ allows_monitors=data.get("allows_monitors", True),
1109
+ timeout=data.get("timeout", 300.0),
1110
+ timestamp=data.get("timestamp", time.time()),
1111
+ )
1112
+
1113
+
1114
+ @dataclass
1115
+ class DeviceReleaseRequest:
1116
+ """Client -> Daemon: Request to release a device lease.
1117
+
1118
+ Attributes:
1119
+ lease_id: The lease ID to release
1120
+ timestamp: Unix timestamp when request was created
1121
+ """
1122
+
1123
+ lease_id: str
1124
+ timestamp: float = field(default_factory=time.time)
1125
+
1126
+ def to_dict(self) -> dict[str, Any]:
1127
+ """Convert to dictionary for JSON serialization."""
1128
+ return asdict(self)
1129
+
1130
+ @classmethod
1131
+ def from_dict(cls, data: dict[str, Any]) -> "DeviceReleaseRequest":
1132
+ """Create DeviceReleaseRequest from dictionary."""
1133
+ return cls(
1134
+ lease_id=data["lease_id"],
1135
+ timestamp=data.get("timestamp", time.time()),
1136
+ )
1137
+
1138
+
1139
+ @dataclass
1140
+ class DevicePreemptRequest:
1141
+ """Client -> Daemon: Request to preempt a device's exclusive holder.
1142
+
1143
+ Forcibly takes the exclusive lease from the current holder.
1144
+ The reason is REQUIRED and must not be empty.
1145
+
1146
+ Attributes:
1147
+ device_id: The device to preempt
1148
+ reason: REQUIRED reason for preemption (must not be empty)
1149
+ timestamp: Unix timestamp when request was created
1150
+ """
1151
+
1152
+ device_id: str
1153
+ reason: str # REQUIRED - must not be empty
1154
+ timestamp: float = field(default_factory=time.time)
1155
+
1156
+ def to_dict(self) -> dict[str, Any]:
1157
+ """Convert to dictionary for JSON serialization."""
1158
+ return asdict(self)
1159
+
1160
+ @classmethod
1161
+ def from_dict(cls, data: dict[str, Any]) -> "DevicePreemptRequest":
1162
+ """Create DevicePreemptRequest from dictionary."""
1163
+ return cls(
1164
+ device_id=data["device_id"],
1165
+ reason=data["reason"],
1166
+ timestamp=data.get("timestamp", time.time()),
1167
+ )
1168
+
1169
+
1170
+ @dataclass
1171
+ class DeviceListRequest:
1172
+ """Client -> Daemon: Request to list all devices.
1173
+
1174
+ Attributes:
1175
+ include_disconnected: Whether to include disconnected devices
1176
+ refresh: Whether to refresh device discovery before listing
1177
+ timestamp: Unix timestamp when request was created
1178
+ """
1179
+
1180
+ include_disconnected: bool = False
1181
+ refresh: bool = False
1182
+ timestamp: float = field(default_factory=time.time)
1183
+
1184
+ def to_dict(self) -> dict[str, Any]:
1185
+ """Convert to dictionary for JSON serialization."""
1186
+ return asdict(self)
1187
+
1188
+ @classmethod
1189
+ def from_dict(cls, data: dict[str, Any]) -> "DeviceListRequest":
1190
+ """Create DeviceListRequest from dictionary."""
1191
+ return cls(
1192
+ include_disconnected=data.get("include_disconnected", False),
1193
+ refresh=data.get("refresh", False),
1194
+ timestamp=data.get("timestamp", time.time()),
1195
+ )
1196
+
1197
+
1198
+ @dataclass
1199
+ class DeviceStatusRequest:
1200
+ """Client -> Daemon: Request for detailed device status.
1201
+
1202
+ Attributes:
1203
+ device_id: The device to get status for
1204
+ timestamp: Unix timestamp when request was created
1205
+ """
1206
+
1207
+ device_id: str
1208
+ timestamp: float = field(default_factory=time.time)
1209
+
1210
+ def to_dict(self) -> dict[str, Any]:
1211
+ """Convert to dictionary for JSON serialization."""
1212
+ return asdict(self)
1213
+
1214
+ @classmethod
1215
+ def from_dict(cls, data: dict[str, Any]) -> "DeviceStatusRequest":
1216
+ """Create DeviceStatusRequest from dictionary."""
1217
+ return cls(
1218
+ device_id=data["device_id"],
1219
+ timestamp=data.get("timestamp", time.time()),
1220
+ )
1221
+
1222
+
1223
+ @dataclass
1224
+ class DeviceLeaseResponse:
1225
+ """Daemon -> Client: Response to device lease operations.
1226
+
1227
+ Attributes:
1228
+ success: Whether the operation succeeded
1229
+ message: Human-readable status message
1230
+ lease_id: The lease ID (if acquired)
1231
+ device_id: The device ID
1232
+ lease_type: Type of lease acquired
1233
+ allows_monitors: Whether monitors are allowed (for exclusive leases)
1234
+ preempted_client_id: Client ID that was preempted (for preempt operations)
1235
+ timestamp: Unix timestamp of the response
1236
+ """
1237
+
1238
+ success: bool
1239
+ message: str
1240
+ lease_id: str | None = None
1241
+ device_id: str | None = None
1242
+ lease_type: str | None = None
1243
+ allows_monitors: bool = True
1244
+ preempted_client_id: str | None = None
1245
+ timestamp: float = field(default_factory=time.time)
1246
+
1247
+ def to_dict(self) -> dict[str, Any]:
1248
+ """Convert to dictionary for JSON serialization."""
1249
+ return asdict(self)
1250
+
1251
+ @classmethod
1252
+ def from_dict(cls, data: dict[str, Any]) -> "DeviceLeaseResponse":
1253
+ """Create DeviceLeaseResponse from dictionary."""
1254
+ return cls(
1255
+ success=data["success"],
1256
+ message=data["message"],
1257
+ lease_id=data.get("lease_id"),
1258
+ device_id=data.get("device_id"),
1259
+ lease_type=data.get("lease_type"),
1260
+ allows_monitors=data.get("allows_monitors", True),
1261
+ preempted_client_id=data.get("preempted_client_id"),
1262
+ timestamp=data.get("timestamp", time.time()),
1263
+ )
1264
+
1265
+
1266
+ @dataclass
1267
+ class DeviceListResponse:
1268
+ """Daemon -> Client: Response to device list request.
1269
+
1270
+ Attributes:
1271
+ success: Whether the operation succeeded
1272
+ message: Human-readable status message
1273
+ devices: List of device information dictionaries
1274
+ total_devices: Total number of devices
1275
+ connected_devices: Number of connected devices
1276
+ total_leases: Total number of active leases
1277
+ timestamp: Unix timestamp of the response
1278
+ """
1279
+
1280
+ success: bool
1281
+ message: str
1282
+ devices: list[dict[str, Any]] = field(default_factory=list)
1283
+ total_devices: int = 0
1284
+ connected_devices: int = 0
1285
+ total_leases: int = 0
1286
+ timestamp: float = field(default_factory=time.time)
1287
+
1288
+ def to_dict(self) -> dict[str, Any]:
1289
+ """Convert to dictionary for JSON serialization."""
1290
+ return asdict(self)
1291
+
1292
+ @classmethod
1293
+ def from_dict(cls, data: dict[str, Any]) -> "DeviceListResponse":
1294
+ """Create DeviceListResponse from dictionary."""
1295
+ return cls(
1296
+ success=data["success"],
1297
+ message=data["message"],
1298
+ devices=data.get("devices", []),
1299
+ total_devices=data.get("total_devices", 0),
1300
+ connected_devices=data.get("connected_devices", 0),
1301
+ total_leases=data.get("total_leases", 0),
1302
+ timestamp=data.get("timestamp", time.time()),
1303
+ )
1304
+
1305
+
1306
+ @dataclass
1307
+ class DeviceStatusResponse:
1308
+ """Daemon -> Client: Response to device status request.
1309
+
1310
+ Attributes:
1311
+ success: Whether the operation succeeded
1312
+ message: Human-readable status message
1313
+ device_id: The device ID
1314
+ exists: Whether the device exists in the inventory
1315
+ is_connected: Whether the device is currently connected
1316
+ device_info: Full device information dictionary
1317
+ exclusive_lease: Current exclusive lease info (if any)
1318
+ monitor_leases: List of monitor lease info dictionaries
1319
+ monitor_count: Number of active monitor leases
1320
+ is_available_for_exclusive: Whether exclusive lease can be acquired
1321
+ timestamp: Unix timestamp of the response
1322
+ """
1323
+
1324
+ success: bool
1325
+ message: str
1326
+ device_id: str = ""
1327
+ exists: bool = False
1328
+ is_connected: bool = False
1329
+ device_info: dict[str, Any] | None = None
1330
+ exclusive_lease: dict[str, Any] | None = None
1331
+ monitor_leases: list[dict[str, Any]] = field(default_factory=list)
1332
+ monitor_count: int = 0
1333
+ is_available_for_exclusive: bool = False
1334
+ timestamp: float = field(default_factory=time.time)
1335
+
1336
+ def to_dict(self) -> dict[str, Any]:
1337
+ """Convert to dictionary for JSON serialization."""
1338
+ return asdict(self)
1339
+
1340
+ @classmethod
1341
+ def from_dict(cls, data: dict[str, Any]) -> "DeviceStatusResponse":
1342
+ """Create DeviceStatusResponse from dictionary."""
1343
+ return cls(
1344
+ success=data["success"],
1345
+ message=data["message"],
1346
+ device_id=data.get("device_id", ""),
1347
+ exists=data.get("exists", False),
1348
+ is_connected=data.get("is_connected", False),
1349
+ device_info=data.get("device_info"),
1350
+ exclusive_lease=data.get("exclusive_lease"),
1351
+ monitor_leases=data.get("monitor_leases", []),
1352
+ monitor_count=data.get("monitor_count", 0),
1353
+ is_available_for_exclusive=data.get("is_available_for_exclusive", False),
1354
+ timestamp=data.get("timestamp", time.time()),
1355
+ )
1356
+
1357
+
1358
+ @dataclass
1359
+ class DevicePreemptNotification:
1360
+ """Daemon -> Client: Notification that device was preempted.
1361
+
1362
+ Sent to the client that was preempted from a device.
1363
+
1364
+ Attributes:
1365
+ device_id: The device that was preempted
1366
+ preempted_by: Client ID of the requester
1367
+ reason: Reason for preemption (required)
1368
+ timestamp: Unix timestamp when preemption occurred
1369
+ """
1370
+
1371
+ device_id: str
1372
+ preempted_by: str
1373
+ reason: str
1374
+ timestamp: float = field(default_factory=time.time)
1375
+
1376
+ def to_dict(self) -> dict[str, Any]:
1377
+ """Convert to dictionary for JSON serialization."""
1378
+ return asdict(self)
1379
+
1380
+ @classmethod
1381
+ def from_dict(cls, data: dict[str, Any]) -> "DevicePreemptNotification":
1382
+ """Create DevicePreemptNotification from dictionary."""
1383
+ return cls(
1384
+ device_id=data["device_id"],
1385
+ preempted_by=data["preempted_by"],
1386
+ reason=data["reason"],
1387
+ timestamp=data.get("timestamp", time.time()),
1388
+ )