fbuild 1.1.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 fbuild might be problematic. Click here for more details.

Files changed (93) hide show
  1. fbuild/__init__.py +0 -0
  2. fbuild/assets/example.txt +1 -0
  3. fbuild/build/__init__.py +117 -0
  4. fbuild/build/archive_creator.py +186 -0
  5. fbuild/build/binary_generator.py +444 -0
  6. fbuild/build/build_component_factory.py +131 -0
  7. fbuild/build/build_state.py +325 -0
  8. fbuild/build/build_utils.py +98 -0
  9. fbuild/build/compilation_executor.py +422 -0
  10. fbuild/build/compiler.py +165 -0
  11. fbuild/build/compiler_avr.py +574 -0
  12. fbuild/build/configurable_compiler.py +612 -0
  13. fbuild/build/configurable_linker.py +637 -0
  14. fbuild/build/flag_builder.py +186 -0
  15. fbuild/build/library_dependency_processor.py +185 -0
  16. fbuild/build/linker.py +708 -0
  17. fbuild/build/orchestrator.py +67 -0
  18. fbuild/build/orchestrator_avr.py +656 -0
  19. fbuild/build/orchestrator_esp32.py +797 -0
  20. fbuild/build/orchestrator_teensy.py +543 -0
  21. fbuild/build/source_compilation_orchestrator.py +220 -0
  22. fbuild/build/source_scanner.py +516 -0
  23. fbuild/cli.py +566 -0
  24. fbuild/cli_utils.py +312 -0
  25. fbuild/config/__init__.py +16 -0
  26. fbuild/config/board_config.py +457 -0
  27. fbuild/config/board_loader.py +92 -0
  28. fbuild/config/ini_parser.py +209 -0
  29. fbuild/config/mcu_specs.py +88 -0
  30. fbuild/daemon/__init__.py +34 -0
  31. fbuild/daemon/client.py +929 -0
  32. fbuild/daemon/compilation_queue.py +293 -0
  33. fbuild/daemon/daemon.py +474 -0
  34. fbuild/daemon/daemon_context.py +196 -0
  35. fbuild/daemon/error_collector.py +263 -0
  36. fbuild/daemon/file_cache.py +332 -0
  37. fbuild/daemon/lock_manager.py +270 -0
  38. fbuild/daemon/logging_utils.py +149 -0
  39. fbuild/daemon/messages.py +301 -0
  40. fbuild/daemon/operation_registry.py +288 -0
  41. fbuild/daemon/process_tracker.py +366 -0
  42. fbuild/daemon/processors/__init__.py +12 -0
  43. fbuild/daemon/processors/build_processor.py +157 -0
  44. fbuild/daemon/processors/deploy_processor.py +327 -0
  45. fbuild/daemon/processors/monitor_processor.py +146 -0
  46. fbuild/daemon/request_processor.py +401 -0
  47. fbuild/daemon/status_manager.py +216 -0
  48. fbuild/daemon/subprocess_manager.py +316 -0
  49. fbuild/deploy/__init__.py +17 -0
  50. fbuild/deploy/deployer.py +67 -0
  51. fbuild/deploy/deployer_esp32.py +314 -0
  52. fbuild/deploy/monitor.py +495 -0
  53. fbuild/interrupt_utils.py +34 -0
  54. fbuild/packages/__init__.py +53 -0
  55. fbuild/packages/archive_utils.py +1098 -0
  56. fbuild/packages/arduino_core.py +412 -0
  57. fbuild/packages/cache.py +249 -0
  58. fbuild/packages/downloader.py +366 -0
  59. fbuild/packages/framework_esp32.py +538 -0
  60. fbuild/packages/framework_teensy.py +346 -0
  61. fbuild/packages/github_utils.py +96 -0
  62. fbuild/packages/header_trampoline_cache.py +394 -0
  63. fbuild/packages/library_compiler.py +203 -0
  64. fbuild/packages/library_manager.py +549 -0
  65. fbuild/packages/library_manager_esp32.py +413 -0
  66. fbuild/packages/package.py +163 -0
  67. fbuild/packages/platform_esp32.py +383 -0
  68. fbuild/packages/platform_teensy.py +312 -0
  69. fbuild/packages/platform_utils.py +131 -0
  70. fbuild/packages/platformio_registry.py +325 -0
  71. fbuild/packages/sdk_utils.py +231 -0
  72. fbuild/packages/toolchain.py +436 -0
  73. fbuild/packages/toolchain_binaries.py +196 -0
  74. fbuild/packages/toolchain_esp32.py +484 -0
  75. fbuild/packages/toolchain_metadata.py +185 -0
  76. fbuild/packages/toolchain_teensy.py +404 -0
  77. fbuild/platform_configs/esp32.json +150 -0
  78. fbuild/platform_configs/esp32c2.json +144 -0
  79. fbuild/platform_configs/esp32c3.json +143 -0
  80. fbuild/platform_configs/esp32c5.json +151 -0
  81. fbuild/platform_configs/esp32c6.json +151 -0
  82. fbuild/platform_configs/esp32p4.json +149 -0
  83. fbuild/platform_configs/esp32s3.json +151 -0
  84. fbuild/platform_configs/imxrt1062.json +56 -0
  85. fbuild-1.1.0.dist-info/METADATA +447 -0
  86. fbuild-1.1.0.dist-info/RECORD +93 -0
  87. fbuild-1.1.0.dist-info/WHEEL +5 -0
  88. fbuild-1.1.0.dist-info/entry_points.txt +5 -0
  89. fbuild-1.1.0.dist-info/licenses/LICENSE +21 -0
  90. fbuild-1.1.0.dist-info/top_level.txt +2 -0
  91. fbuild_lint/__init__.py +0 -0
  92. fbuild_lint/ruff_plugins/__init__.py +0 -0
  93. fbuild_lint/ruff_plugins/keyboard_interrupt_checker.py +158 -0
@@ -0,0 +1,316 @@
1
+ """Centralized subprocess execution manager for daemon operations.
2
+
3
+ This module provides a unified interface for executing subprocesses with tracking,
4
+ logging, and statistics. All subprocess calls should go through this manager for
5
+ consistent error handling and monitoring.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import subprocess
12
+ import threading
13
+ import time
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any, Optional
17
+
18
+ from ..interrupt_utils import handle_keyboard_interrupt_properly
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass
24
+ class SubprocessExecution:
25
+ """Single subprocess execution with full tracking."""
26
+
27
+ execution_id: str
28
+ command: list[str]
29
+ cwd: Optional[Path]
30
+ env: Optional[dict[str, str]]
31
+ timeout: Optional[float]
32
+ returncode: Optional[int] = None
33
+ stdout: Optional[str] = None
34
+ stderr: Optional[str] = None
35
+ start_time: Optional[float] = None
36
+ end_time: Optional[float] = None
37
+ error: Optional[str] = None
38
+
39
+ def duration(self) -> Optional[float]:
40
+ """Calculate execution duration in seconds."""
41
+ if self.start_time and self.end_time:
42
+ return self.end_time - self.start_time
43
+ return None
44
+
45
+ def success(self) -> bool:
46
+ """Check if execution was successful."""
47
+ return self.returncode == 0 and self.error is None
48
+
49
+
50
+ class SubprocessManager:
51
+ """Centralized subprocess execution manager.
52
+
53
+ Provides tracking, logging, and statistics for all subprocess executions
54
+ in the daemon. Thread-safe for concurrent use.
55
+ """
56
+
57
+ def __init__(self, max_history: int = 1000):
58
+ """Initialize subprocess manager.
59
+
60
+ Args:
61
+ max_history: Maximum number of executions to keep in history
62
+ """
63
+ self.executions: dict[str, SubprocessExecution] = {}
64
+ self.lock = threading.Lock()
65
+ self.max_history = max_history
66
+ self._execution_counter = 0
67
+ logger.info(f"SubprocessManager initialized successfully (max_history={max_history})")
68
+
69
+ def execute(
70
+ self,
71
+ command: list[str],
72
+ cwd: Optional[Path] = None,
73
+ env: Optional[dict[str, str]] = None,
74
+ timeout: Optional[float] = 60,
75
+ capture_output: bool = True,
76
+ check: bool = False,
77
+ ) -> SubprocessExecution:
78
+ """Execute subprocess with tracking.
79
+
80
+ Args:
81
+ command: Command and arguments to execute
82
+ cwd: Working directory for subprocess
83
+ env: Environment variables
84
+ timeout: Timeout in seconds (None = no timeout)
85
+ capture_output: Whether to capture stdout/stderr
86
+ check: Whether to raise exception on non-zero exit code
87
+
88
+ Returns:
89
+ SubprocessExecution with results and timing information
90
+ """
91
+ # Generate unique execution ID
92
+ with self.lock:
93
+ self._execution_counter += 1
94
+ execution_id = f"subprocess_{int(time.time() * 1000000)}_{self._execution_counter}"
95
+ logger.debug(f"Subprocess command: {' '.join(str(c) for c in command)}")
96
+ logger.debug(f"Environment variables: {len(env) if env else 0} vars")
97
+ logger.debug(f"Capture output: {capture_output}")
98
+
99
+ execution = SubprocessExecution(
100
+ execution_id=execution_id,
101
+ command=command,
102
+ cwd=cwd,
103
+ env=env,
104
+ timeout=timeout,
105
+ start_time=time.time(),
106
+ )
107
+
108
+ # Store execution
109
+ with self.lock:
110
+ self.executions[execution_id] = execution
111
+ logger.debug(f"Stored execution {execution_id} in history (total: {len(self.executions)})")
112
+ self._cleanup_old_executions()
113
+
114
+ # Log execution start
115
+ cmd_str = " ".join(str(c) for c in command[:3]) # First 3 args
116
+ if len(command) > 3:
117
+ cmd_str += "..."
118
+ logger.info(f"Starting subprocess {execution_id}: {cmd_str}")
119
+
120
+ try:
121
+ # Execute subprocess
122
+ logger.debug(f"Executing subprocess.run() for {execution_id}")
123
+ result = subprocess.run(
124
+ command,
125
+ cwd=cwd,
126
+ env=env,
127
+ capture_output=capture_output,
128
+ text=True,
129
+ timeout=timeout,
130
+ check=check,
131
+ )
132
+ execution.returncode = result.returncode
133
+ execution.stdout = result.stdout if capture_output else None
134
+ execution.stderr = result.stderr if capture_output else None
135
+ execution.end_time = time.time()
136
+
137
+ # Log output details
138
+ if capture_output:
139
+ logger.debug(f"Subprocess {execution_id} stdout: {len(result.stdout) if result.stdout else 0} bytes")
140
+ logger.debug(f"Subprocess {execution_id} stderr: {len(result.stderr) if result.stderr else 0} bytes")
141
+
142
+ # Log result
143
+ duration = execution.duration()
144
+ if result.returncode == 0:
145
+ logger.info(f"Subprocess {execution_id}: SUCCESS in {duration:.2f}s")
146
+ else:
147
+ logger.warning(f"Subprocess {execution_id}: FAILED with code {result.returncode} in {duration:.2f}s")
148
+ if result.stderr and capture_output:
149
+ logger.debug(f"Subprocess {execution_id} stderr: {result.stderr[:200]}")
150
+
151
+ except subprocess.TimeoutExpired as e:
152
+ logger.error(f"Subprocess {execution_id}: TIMEOUT after {timeout}s")
153
+ execution.error = f"Timeout after {timeout}s"
154
+ execution.returncode = -1
155
+ execution.stderr = str(e)
156
+ execution.end_time = time.time()
157
+
158
+ except subprocess.CalledProcessError as e:
159
+ logger.error(f"Subprocess {execution_id}: CalledProcessError with exit code {e.returncode}")
160
+ execution.error = f"Process failed with exit code {e.returncode}"
161
+ execution.returncode = e.returncode
162
+ execution.stdout = e.stdout if capture_output else None
163
+ execution.stderr = e.stderr if capture_output else None
164
+ execution.end_time = time.time()
165
+
166
+ except KeyboardInterrupt as ke:
167
+ logger.warning(f"Subprocess {execution_id}: KeyboardInterrupt received")
168
+ handle_keyboard_interrupt_properly(ke)
169
+
170
+ except Exception as e:
171
+ logger.error(f"Subprocess {execution_id}: Unexpected exception: {e}", exc_info=True)
172
+ execution.error = str(e)
173
+ execution.returncode = -1
174
+ execution.end_time = time.time()
175
+
176
+ logger.debug(f"Returning execution result for {execution_id} (success={execution.success()})")
177
+ return execution
178
+
179
+ def get_execution(self, execution_id: str) -> Optional[SubprocessExecution]:
180
+ """Get execution by ID.
181
+
182
+ Args:
183
+ execution_id: Execution ID to retrieve
184
+
185
+ Returns:
186
+ SubprocessExecution if found, None otherwise
187
+ """
188
+ with self.lock:
189
+ execution = self.executions.get(execution_id)
190
+ if execution:
191
+ logger.debug(f"Found execution {execution_id} (success={execution.success()})")
192
+ else:
193
+ logger.debug(f"Execution {execution_id} not found")
194
+ return execution
195
+
196
+ def get_statistics(self) -> dict[str, Any]:
197
+ """Get subprocess execution statistics.
198
+
199
+ Returns:
200
+ Dictionary with execution counts and statistics
201
+ """
202
+ with self.lock:
203
+ total = len(self.executions)
204
+ successful = sum(1 for e in self.executions.values() if e.success())
205
+ failed = sum(1 for e in self.executions.values() if not e.success())
206
+
207
+ # Calculate average duration for successful executions
208
+ successful_durations: list[float] = []
209
+ for e in self.executions.values():
210
+ if e.success():
211
+ duration = e.duration()
212
+ if duration is not None:
213
+ successful_durations.append(duration)
214
+ avg_duration = sum(successful_durations) / len(successful_durations) if successful_durations else 0.0
215
+
216
+ logger.debug(f"Average execution duration: {avg_duration:.3f}s (from {len(successful_durations)} successful executions)")
217
+
218
+ success_rate = (successful / total * 100) if total > 0 else 0.0
219
+ logger.info(f"Subprocess statistics: {successful}/{total} successful ({success_rate:.1f}% success rate)")
220
+
221
+ return {
222
+ "total_executions": total,
223
+ "successful": successful,
224
+ "failed": failed,
225
+ "average_duration_seconds": round(avg_duration, 3),
226
+ }
227
+
228
+ def get_recent_failures(self, count: int = 10) -> list[SubprocessExecution]:
229
+ """Get most recent failed executions.
230
+
231
+ Args:
232
+ count: Maximum number of failures to return
233
+
234
+ Returns:
235
+ List of failed SubprocessExecution objects
236
+ """
237
+ with self.lock:
238
+ failures = [e for e in self.executions.values() if not e.success()]
239
+ logger.debug(f"Found {len(failures)} total failures in history")
240
+ # Sort by end_time descending (most recent first)
241
+ failures.sort(key=lambda e: e.end_time or 0, reverse=True)
242
+ result = failures[:count]
243
+ logger.debug(f"Returning {len(result)} most recent failures")
244
+ if result:
245
+ logger.info(f"Recent subprocess failures: {len(result)} failures found")
246
+ return result
247
+
248
+ def clear_history(self):
249
+ """Clear all execution history."""
250
+ with self.lock:
251
+ count = len(self.executions)
252
+ self.executions.clear()
253
+ logger.info(f"Subprocess execution history cleared ({count} records removed)")
254
+
255
+ def _cleanup_old_executions(self):
256
+ """Remove old executions beyond max_history limit.
257
+
258
+ Keeps successful executions to max_history, but always keeps all recent failures.
259
+ """
260
+ current_count = len(self.executions)
261
+ if current_count <= self.max_history:
262
+ return
263
+
264
+ # Get all executions sorted by end time
265
+ all_executions = sorted(self.executions.values(), key=lambda e: e.end_time or 0)
266
+ logger.debug(f"Sorted {len(all_executions)} executions by end time")
267
+
268
+ # Keep all failures and recent successes
269
+ successes = [e for e in all_executions if e.success()]
270
+ failures = [e for e in all_executions if not e.success()]
271
+ logger.debug(f"Execution breakdown: {len(successes)} successes, {len(failures)} failures")
272
+
273
+ # Remove oldest successes if we're over limit
274
+ to_remove = len(self.executions) - self.max_history
275
+ if to_remove > 0 and len(successes) > to_remove:
276
+ for execution in successes[:to_remove]:
277
+ del self.executions[execution.execution_id]
278
+
279
+ logger.info(f"Cleaned up {to_remove} old successful subprocess executions (history now: {len(self.executions)})")
280
+ else:
281
+ logger.debug(f"Cannot remove {to_remove} executions (only {len(successes)} successes available)")
282
+
283
+
284
+ # Global subprocess manager instance (initialized by daemon)
285
+ _subprocess_manager: Optional[SubprocessManager] = None
286
+
287
+
288
+ def get_subprocess_manager() -> SubprocessManager:
289
+ """Get global subprocess manager instance.
290
+
291
+ Returns:
292
+ Global SubprocessManager instance
293
+
294
+ Raises:
295
+ RuntimeError: If subprocess manager not initialized
296
+ """
297
+ global _subprocess_manager
298
+ if _subprocess_manager is None:
299
+ logger.error("SubprocessManager accessed before initialization")
300
+ raise RuntimeError("SubprocessManager not initialized. Call init_subprocess_manager() first.")
301
+ return _subprocess_manager
302
+
303
+
304
+ def init_subprocess_manager(max_history: int = 1000) -> SubprocessManager:
305
+ """Initialize global subprocess manager.
306
+
307
+ Args:
308
+ max_history: Maximum number of executions to keep in history
309
+
310
+ Returns:
311
+ Initialized SubprocessManager instance
312
+ """
313
+ global _subprocess_manager
314
+ _subprocess_manager = SubprocessManager(max_history=max_history)
315
+ logger.info("Global SubprocessManager initialized successfully")
316
+ return _subprocess_manager
@@ -0,0 +1,17 @@
1
+ """
2
+ Firmware deployment functionality for fbuild.
3
+
4
+ This module provides deployment capabilities for uploading firmware to devices.
5
+ """
6
+
7
+ from .deployer import DeploymentError, DeploymentResult, IDeployer
8
+ from .deployer_esp32 import ESP32Deployer
9
+ from .monitor import SerialMonitor
10
+
11
+ __all__ = [
12
+ "IDeployer",
13
+ "ESP32Deployer",
14
+ "DeploymentResult",
15
+ "DeploymentError",
16
+ "SerialMonitor",
17
+ ]
@@ -0,0 +1,67 @@
1
+ """Abstract base class for firmware deployers.
2
+
3
+ This module defines the interface for platform-specific deployers
4
+ to ensure consistent behavior across different platforms.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ @dataclass
14
+ class DeploymentResult:
15
+ """Result of a firmware deployment operation."""
16
+
17
+ success: bool
18
+ message: str
19
+ port: Optional[str] = None
20
+
21
+
22
+ class DeploymentError(Exception):
23
+ """Base exception for deployment errors."""
24
+
25
+ pass
26
+
27
+
28
+ class IDeployer(ABC):
29
+ """Interface for firmware deployers.
30
+
31
+ Deployers handle uploading firmware to embedded devices:
32
+ 1. Locate firmware binaries
33
+ 2. Detect or validate serial port
34
+ 3. Flash firmware to device
35
+ 4. Verify upload success
36
+ """
37
+
38
+ @abstractmethod
39
+ def deploy(
40
+ self,
41
+ project_dir: Path,
42
+ env_name: str,
43
+ port: Optional[str] = None,
44
+ ) -> DeploymentResult:
45
+ """Deploy firmware to a device.
46
+
47
+ Args:
48
+ project_dir: Path to project directory
49
+ env_name: Environment name to deploy
50
+ port: Serial port to use (auto-detect if None)
51
+
52
+ Returns:
53
+ DeploymentResult with success status and message
54
+
55
+ Raises:
56
+ DeploymentError: If deployment fails
57
+ """
58
+ pass
59
+
60
+ @abstractmethod
61
+ def _detect_serial_port(self) -> Optional[str]:
62
+ """Auto-detect serial port for device.
63
+
64
+ Returns:
65
+ Serial port name or None if not found
66
+ """
67
+ pass