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
@@ -1,263 +1,263 @@
1
- """
2
- Error Collector - Structured error collection for async operations.
3
-
4
- This module provides error collection and aggregation for asynchronous build
5
- operations, replacing simple exception handling with structured error tracking.
6
- """
7
-
8
- import logging
9
- import threading
10
- import time
11
- from dataclasses import dataclass, field
12
- from enum import Enum
13
- from typing import Optional
14
-
15
-
16
- class ErrorSeverity(Enum):
17
- """Severity level of a build error."""
18
-
19
- WARNING = "warning"
20
- ERROR = "error"
21
- FATAL = "fatal"
22
-
23
-
24
- @dataclass
25
- class BuildError:
26
- """Single build error."""
27
-
28
- severity: ErrorSeverity
29
- phase: str # "download", "compile", "link", "upload"
30
- file_path: Optional[str]
31
- error_message: str
32
- stderr: Optional[str] = None
33
- stdout: Optional[str] = None
34
- timestamp: float = field(default_factory=time.time)
35
-
36
- def format(self) -> str:
37
- """Format error as human-readable string.
38
-
39
- Returns:
40
- Formatted error message
41
- """
42
- lines = [f"[{self.severity.value.upper()}] {self.phase}: {self.error_message}"]
43
-
44
- if self.file_path:
45
- lines.append(f" File: {self.file_path}")
46
-
47
- if self.stderr:
48
- # Truncate stderr to reasonable length
49
- stderr_preview = self.stderr[:500]
50
- if len(self.stderr) > 500:
51
- stderr_preview += "... (truncated)"
52
- lines.append(f" stderr: {stderr_preview}")
53
-
54
- return "\n".join(lines)
55
-
56
-
57
- class ErrorCollector:
58
- """Collects errors during async build operations."""
59
-
60
- def __init__(self, max_errors: int = 100):
61
- """Initialize error collector.
62
-
63
- Args:
64
- max_errors: Maximum number of errors to collect
65
- """
66
- self.errors: list[BuildError] = []
67
- self.lock = threading.Lock()
68
- self.max_errors = max_errors
69
-
70
- logging.debug(f"ErrorCollector initialized (max_errors={max_errors})")
71
-
72
- def add_error(self, error: BuildError) -> None:
73
- """Add error to collection.
74
-
75
- Args:
76
- error: Build error to add
77
- """
78
- with self.lock:
79
- if len(self.errors) >= self.max_errors:
80
- logging.warning(f"ErrorCollector full ({self.max_errors} errors), dropping oldest")
81
- self.errors.pop(0)
82
-
83
- self.errors.append(error)
84
-
85
- def get_errors(self, severity: Optional[ErrorSeverity] = None) -> list[BuildError]:
86
- """Get all errors, optionally filtered by severity.
87
-
88
- Args:
89
- severity: Filter by severity (None = all errors)
90
-
91
- Returns:
92
- List of build errors
93
- """
94
- logging.debug(f"Retrieving errors (severity filter: {severity.value if severity else 'None'})")
95
- with self.lock:
96
- if severity:
97
- filtered = [e for e in self.errors if e.severity == severity]
98
- logging.debug(f"Filtered {len(filtered)} errors by severity {severity.value} (total: {len(self.errors)})")
99
- return filtered
100
- logging.debug(f"Returning all {len(self.errors)} errors")
101
- return self.errors.copy()
102
-
103
- def get_errors_by_phase(self, phase: str) -> list[BuildError]:
104
- """Get errors for a specific phase.
105
-
106
- Args:
107
- phase: Phase to filter by
108
-
109
- Returns:
110
- List of build errors for the phase
111
- """
112
- with self.lock:
113
- phase_errors = [e for e in self.errors if e.phase == phase]
114
- logging.debug(f"Found {len(phase_errors)} errors in phase '{phase}' (total: {len(self.errors)})")
115
- return phase_errors
116
-
117
- def has_fatal_errors(self) -> bool:
118
- """Check if any fatal errors occurred.
119
-
120
- Returns:
121
- True if fatal errors exist
122
- """
123
- with self.lock:
124
- has_fatal = any(e.severity == ErrorSeverity.FATAL for e in self.errors)
125
- fatal_count = sum(1 for e in self.errors if e.severity == ErrorSeverity.FATAL)
126
- logging.debug(f"Fatal error check result: {has_fatal} ({fatal_count} fatal errors)")
127
- return has_fatal
128
-
129
- def has_errors(self) -> bool:
130
- """Check if any errors (non-warning) occurred.
131
-
132
- Returns:
133
- True if errors exist
134
- """
135
- logging.debug("Checking for errors (non-warning)")
136
- with self.lock:
137
- has_errs = any(e.severity in (ErrorSeverity.ERROR, ErrorSeverity.FATAL) for e in self.errors)
138
- error_count = sum(1 for e in self.errors if e.severity in (ErrorSeverity.ERROR, ErrorSeverity.FATAL))
139
- logging.debug(f"Error check result: {has_errs} ({error_count} errors or fatal)")
140
- return has_errs
141
-
142
- def has_warnings(self) -> bool:
143
- """Check if any warnings occurred.
144
-
145
- Returns:
146
- True if warnings exist
147
- """
148
- with self.lock:
149
- has_warn = any(e.severity == ErrorSeverity.WARNING for e in self.errors)
150
- warning_count = sum(1 for e in self.errors if e.severity == ErrorSeverity.WARNING)
151
- logging.debug(f"Warning check result: {has_warn} ({warning_count} warnings)")
152
- return has_warn
153
-
154
- def get_error_count(self) -> dict[str, int]:
155
- """Get count of errors by severity.
156
-
157
- Returns:
158
- Dictionary with counts by severity
159
- """
160
- with self.lock:
161
- counts = {
162
- "warnings": sum(1 for e in self.errors if e.severity == ErrorSeverity.WARNING),
163
- "errors": sum(1 for e in self.errors if e.severity == ErrorSeverity.ERROR),
164
- "fatal": sum(1 for e in self.errors if e.severity == ErrorSeverity.FATAL),
165
- "total": len(self.errors),
166
- }
167
- logging.debug(f"Error counts: {counts['total']} total ({counts['fatal']} fatal, {counts['errors']} errors, {counts['warnings']} warnings)")
168
- return counts
169
-
170
- def format_errors(self, max_errors: Optional[int] = None) -> str:
171
- """Format all errors as human-readable string.
172
-
173
- Args:
174
- max_errors: Maximum number of errors to include (None = all)
175
-
176
- Returns:
177
- Formatted error report
178
- """
179
- logging.debug(f"Formatting errors (max_errors: {max_errors})")
180
- with self.lock:
181
- if not self.errors:
182
- return "No errors"
183
-
184
- errors_to_show = self.errors if max_errors is None else self.errors[:max_errors]
185
- logging.debug(f"Formatting {len(errors_to_show)} errors (total: {len(self.errors)})")
186
- lines = []
187
-
188
- for err in errors_to_show:
189
- lines.append(err.format())
190
-
191
- if max_errors and len(self.errors) > max_errors:
192
- lines.append(f"\n... and {len(self.errors) - max_errors} more errors")
193
-
194
- # Add summary
195
- counts = self.get_error_count()
196
- summary = f"\nSummary: {counts['fatal']} fatal, {counts['errors']} errors, {counts['warnings']} warnings"
197
- lines.append(summary)
198
-
199
- formatted = "\n\n".join(lines)
200
- logging.debug(f"Error formatting complete: {len(lines)} sections, {len(formatted)} characters")
201
- return formatted
202
-
203
- def format_summary(self) -> str:
204
- """Format a brief summary of errors.
205
-
206
- Returns:
207
- Brief error summary
208
- """
209
- counts = self.get_error_count()
210
- if counts["total"] == 0:
211
- return "No errors"
212
-
213
- parts = []
214
- if counts["fatal"] > 0:
215
- parts.append(f"{counts['fatal']} fatal")
216
- if counts["errors"] > 0:
217
- parts.append(f"{counts['errors']} errors")
218
- if counts["warnings"] > 0:
219
- parts.append(f"{counts['warnings']} warnings")
220
-
221
- summary = ", ".join(parts)
222
- return summary
223
-
224
- def clear(self) -> None:
225
- """Clear all collected errors."""
226
- with self.lock:
227
- error_count = len(self.errors)
228
- self.errors.clear()
229
-
230
- if error_count > 0:
231
- logging.info(f"Cleared {error_count} errors")
232
-
233
- def get_first_fatal_error(self) -> Optional[BuildError]:
234
- """Get the first fatal error encountered.
235
-
236
- Returns:
237
- First fatal error or None
238
- """
239
- with self.lock:
240
- for error in self.errors:
241
- if error.severity == ErrorSeverity.FATAL:
242
- return error
243
- return None
244
-
245
- def get_compilation_errors(self) -> list[BuildError]:
246
- """Get all compilation-phase errors.
247
-
248
- Returns:
249
- List of compilation errors
250
- """
251
- compilation_errors = self.get_errors_by_phase("compile")
252
- logging.debug(f"Found {len(compilation_errors)} compilation errors")
253
- return compilation_errors
254
-
255
- def get_link_errors(self) -> list[BuildError]:
256
- """Get all link-phase errors.
257
-
258
- Returns:
259
- List of link errors
260
- """
261
- link_errors = self.get_errors_by_phase("link")
262
- logging.debug(f"Found {len(link_errors)} link errors")
263
- return link_errors
1
+ """
2
+ Error Collector - Structured error collection for async operations.
3
+
4
+ This module provides error collection and aggregation for asynchronous build
5
+ operations, replacing simple exception handling with structured error tracking.
6
+ """
7
+
8
+ import logging
9
+ import threading
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from typing import Optional
14
+
15
+
16
+ class ErrorSeverity(Enum):
17
+ """Severity level of a build error."""
18
+
19
+ WARNING = "warning"
20
+ ERROR = "error"
21
+ FATAL = "fatal"
22
+
23
+
24
+ @dataclass
25
+ class BuildError:
26
+ """Single build error."""
27
+
28
+ severity: ErrorSeverity
29
+ phase: str # "download", "compile", "link", "upload"
30
+ file_path: Optional[str]
31
+ error_message: str
32
+ stderr: Optional[str] = None
33
+ stdout: Optional[str] = None
34
+ timestamp: float = field(default_factory=time.time)
35
+
36
+ def format(self) -> str:
37
+ """Format error as human-readable string.
38
+
39
+ Returns:
40
+ Formatted error message
41
+ """
42
+ lines = [f"[{self.severity.value.upper()}] {self.phase}: {self.error_message}"]
43
+
44
+ if self.file_path:
45
+ lines.append(f" File: {self.file_path}")
46
+
47
+ if self.stderr:
48
+ # Truncate stderr to reasonable length
49
+ stderr_preview = self.stderr[:500]
50
+ if len(self.stderr) > 500:
51
+ stderr_preview += "... (truncated)"
52
+ lines.append(f" stderr: {stderr_preview}")
53
+
54
+ return "\n".join(lines)
55
+
56
+
57
+ class ErrorCollector:
58
+ """Collects errors during async build operations."""
59
+
60
+ def __init__(self, max_errors: int = 100):
61
+ """Initialize error collector.
62
+
63
+ Args:
64
+ max_errors: Maximum number of errors to collect
65
+ """
66
+ self.errors: list[BuildError] = []
67
+ self.lock = threading.Lock()
68
+ self.max_errors = max_errors
69
+
70
+ logging.debug(f"ErrorCollector initialized (max_errors={max_errors})")
71
+
72
+ def add_error(self, error: BuildError) -> None:
73
+ """Add error to collection.
74
+
75
+ Args:
76
+ error: Build error to add
77
+ """
78
+ with self.lock:
79
+ if len(self.errors) >= self.max_errors:
80
+ logging.warning(f"ErrorCollector full ({self.max_errors} errors), dropping oldest")
81
+ self.errors.pop(0)
82
+
83
+ self.errors.append(error)
84
+
85
+ def get_errors(self, severity: Optional[ErrorSeverity] = None) -> list[BuildError]:
86
+ """Get all errors, optionally filtered by severity.
87
+
88
+ Args:
89
+ severity: Filter by severity (None = all errors)
90
+
91
+ Returns:
92
+ List of build errors
93
+ """
94
+ logging.debug(f"Retrieving errors (severity filter: {severity.value if severity else 'None'})")
95
+ with self.lock:
96
+ if severity:
97
+ filtered = [e for e in self.errors if e.severity == severity]
98
+ logging.debug(f"Filtered {len(filtered)} errors by severity {severity.value} (total: {len(self.errors)})")
99
+ return filtered
100
+ logging.debug(f"Returning all {len(self.errors)} errors")
101
+ return self.errors.copy()
102
+
103
+ def get_errors_by_phase(self, phase: str) -> list[BuildError]:
104
+ """Get errors for a specific phase.
105
+
106
+ Args:
107
+ phase: Phase to filter by
108
+
109
+ Returns:
110
+ List of build errors for the phase
111
+ """
112
+ with self.lock:
113
+ phase_errors = [e for e in self.errors if e.phase == phase]
114
+ logging.debug(f"Found {len(phase_errors)} errors in phase '{phase}' (total: {len(self.errors)})")
115
+ return phase_errors
116
+
117
+ def has_fatal_errors(self) -> bool:
118
+ """Check if any fatal errors occurred.
119
+
120
+ Returns:
121
+ True if fatal errors exist
122
+ """
123
+ with self.lock:
124
+ has_fatal = any(e.severity == ErrorSeverity.FATAL for e in self.errors)
125
+ fatal_count = sum(1 for e in self.errors if e.severity == ErrorSeverity.FATAL)
126
+ logging.debug(f"Fatal error check result: {has_fatal} ({fatal_count} fatal errors)")
127
+ return has_fatal
128
+
129
+ def has_errors(self) -> bool:
130
+ """Check if any errors (non-warning) occurred.
131
+
132
+ Returns:
133
+ True if errors exist
134
+ """
135
+ logging.debug("Checking for errors (non-warning)")
136
+ with self.lock:
137
+ has_errs = any(e.severity in (ErrorSeverity.ERROR, ErrorSeverity.FATAL) for e in self.errors)
138
+ error_count = sum(1 for e in self.errors if e.severity in (ErrorSeverity.ERROR, ErrorSeverity.FATAL))
139
+ logging.debug(f"Error check result: {has_errs} ({error_count} errors or fatal)")
140
+ return has_errs
141
+
142
+ def has_warnings(self) -> bool:
143
+ """Check if any warnings occurred.
144
+
145
+ Returns:
146
+ True if warnings exist
147
+ """
148
+ with self.lock:
149
+ has_warn = any(e.severity == ErrorSeverity.WARNING for e in self.errors)
150
+ warning_count = sum(1 for e in self.errors if e.severity == ErrorSeverity.WARNING)
151
+ logging.debug(f"Warning check result: {has_warn} ({warning_count} warnings)")
152
+ return has_warn
153
+
154
+ def get_error_count(self) -> dict[str, int]:
155
+ """Get count of errors by severity.
156
+
157
+ Returns:
158
+ Dictionary with counts by severity
159
+ """
160
+ with self.lock:
161
+ counts = {
162
+ "warnings": sum(1 for e in self.errors if e.severity == ErrorSeverity.WARNING),
163
+ "errors": sum(1 for e in self.errors if e.severity == ErrorSeverity.ERROR),
164
+ "fatal": sum(1 for e in self.errors if e.severity == ErrorSeverity.FATAL),
165
+ "total": len(self.errors),
166
+ }
167
+ logging.debug(f"Error counts: {counts['total']} total ({counts['fatal']} fatal, {counts['errors']} errors, {counts['warnings']} warnings)")
168
+ return counts
169
+
170
+ def format_errors(self, max_errors: Optional[int] = None) -> str:
171
+ """Format all errors as human-readable string.
172
+
173
+ Args:
174
+ max_errors: Maximum number of errors to include (None = all)
175
+
176
+ Returns:
177
+ Formatted error report
178
+ """
179
+ logging.debug(f"Formatting errors (max_errors: {max_errors})")
180
+ with self.lock:
181
+ if not self.errors:
182
+ return "No errors"
183
+
184
+ errors_to_show = self.errors if max_errors is None else self.errors[:max_errors]
185
+ logging.debug(f"Formatting {len(errors_to_show)} errors (total: {len(self.errors)})")
186
+ lines = []
187
+
188
+ for err in errors_to_show:
189
+ lines.append(err.format())
190
+
191
+ if max_errors and len(self.errors) > max_errors:
192
+ lines.append(f"\n... and {len(self.errors) - max_errors} more errors")
193
+
194
+ # Add summary
195
+ counts = self.get_error_count()
196
+ summary = f"\nSummary: {counts['fatal']} fatal, {counts['errors']} errors, {counts['warnings']} warnings"
197
+ lines.append(summary)
198
+
199
+ formatted = "\n\n".join(lines)
200
+ logging.debug(f"Error formatting complete: {len(lines)} sections, {len(formatted)} characters")
201
+ return formatted
202
+
203
+ def format_summary(self) -> str:
204
+ """Format a brief summary of errors.
205
+
206
+ Returns:
207
+ Brief error summary
208
+ """
209
+ counts = self.get_error_count()
210
+ if counts["total"] == 0:
211
+ return "No errors"
212
+
213
+ parts = []
214
+ if counts["fatal"] > 0:
215
+ parts.append(f"{counts['fatal']} fatal")
216
+ if counts["errors"] > 0:
217
+ parts.append(f"{counts['errors']} errors")
218
+ if counts["warnings"] > 0:
219
+ parts.append(f"{counts['warnings']} warnings")
220
+
221
+ summary = ", ".join(parts)
222
+ return summary
223
+
224
+ def clear(self) -> None:
225
+ """Clear all collected errors."""
226
+ with self.lock:
227
+ error_count = len(self.errors)
228
+ self.errors.clear()
229
+
230
+ if error_count > 0:
231
+ logging.info(f"Cleared {error_count} errors")
232
+
233
+ def get_first_fatal_error(self) -> Optional[BuildError]:
234
+ """Get the first fatal error encountered.
235
+
236
+ Returns:
237
+ First fatal error or None
238
+ """
239
+ with self.lock:
240
+ for error in self.errors:
241
+ if error.severity == ErrorSeverity.FATAL:
242
+ return error
243
+ return None
244
+
245
+ def get_compilation_errors(self) -> list[BuildError]:
246
+ """Get all compilation-phase errors.
247
+
248
+ Returns:
249
+ List of compilation errors
250
+ """
251
+ compilation_errors = self.get_errors_by_phase("compile")
252
+ logging.debug(f"Found {len(compilation_errors)} compilation errors")
253
+ return compilation_errors
254
+
255
+ def get_link_errors(self) -> list[BuildError]:
256
+ """Get all link-phase errors.
257
+
258
+ Returns:
259
+ List of link errors
260
+ """
261
+ link_errors = self.get_errors_by_phase("link")
262
+ logging.debug(f"Found {len(link_errors)} link errors")
263
+ return link_errors