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,325 @@
1
+ """Build state tracking for cache invalidation.
2
+
3
+ This module tracks build configuration state to detect when builds need to be
4
+ invalidated due to changes in:
5
+ - platformio.ini configuration
6
+ - Framework versions
7
+ - Platform versions
8
+ - Toolchain versions
9
+ - Library dependencies
10
+
11
+ Design:
12
+ - Stores build state metadata in .fbuild/build/{env_name}/build_state.json
13
+ - Compares current state with saved state to detect changes
14
+ - Provides clear reasons when invalidation is needed
15
+ """
16
+
17
+ import hashlib
18
+ import json
19
+ from pathlib import Path
20
+ from typing import Dict, List, Optional, Tuple
21
+
22
+
23
+ class BuildStateError(Exception):
24
+ """Raised when build state operations fail."""
25
+ pass
26
+
27
+
28
+ class BuildState:
29
+ """Represents the current state of a build configuration."""
30
+
31
+ def __init__(
32
+ self,
33
+ platformio_ini_hash: str,
34
+ platform: str,
35
+ board: str,
36
+ framework: str,
37
+ toolchain_version: Optional[str] = None,
38
+ framework_version: Optional[str] = None,
39
+ platform_version: Optional[str] = None,
40
+ build_flags: Optional[List[str]] = None,
41
+ lib_deps: Optional[List[str]] = None,
42
+ ):
43
+ """Initialize build state.
44
+
45
+ Args:
46
+ platformio_ini_hash: SHA256 hash of platformio.ini content
47
+ platform: Platform name (e.g., 'atmelavr', 'espressif32')
48
+ board: Board ID (e.g., 'uno', 'esp32dev')
49
+ framework: Framework name (e.g., 'arduino')
50
+ toolchain_version: Version of toolchain (e.g., '7.3.0-atmel3.6.1-arduino7')
51
+ framework_version: Version of framework (e.g., '1.8.6')
52
+ platform_version: Version of platform package
53
+ build_flags: List of build flags from platformio.ini
54
+ lib_deps: List of library dependencies from platformio.ini
55
+ """
56
+ self.platformio_ini_hash = platformio_ini_hash
57
+ self.platform = platform
58
+ self.board = board
59
+ self.framework = framework
60
+ self.toolchain_version = toolchain_version
61
+ self.framework_version = framework_version
62
+ self.platform_version = platform_version
63
+ self.build_flags = build_flags or []
64
+ self.lib_deps = lib_deps or []
65
+
66
+ def to_dict(self) -> Dict:
67
+ """Convert to dictionary for JSON serialization."""
68
+ return {
69
+ "platformio_ini_hash": self.platformio_ini_hash,
70
+ "platform": self.platform,
71
+ "board": self.board,
72
+ "framework": self.framework,
73
+ "toolchain_version": self.toolchain_version,
74
+ "framework_version": self.framework_version,
75
+ "platform_version": self.platform_version,
76
+ "build_flags": self.build_flags,
77
+ "lib_deps": self.lib_deps,
78
+ }
79
+
80
+ @classmethod
81
+ def from_dict(cls, data: Dict) -> "BuildState":
82
+ """Create from dictionary."""
83
+ return cls(
84
+ platformio_ini_hash=data["platformio_ini_hash"],
85
+ platform=data["platform"],
86
+ board=data["board"],
87
+ framework=data["framework"],
88
+ toolchain_version=data.get("toolchain_version"),
89
+ framework_version=data.get("framework_version"),
90
+ platform_version=data.get("platform_version"),
91
+ build_flags=data.get("build_flags", []),
92
+ lib_deps=data.get("lib_deps", []),
93
+ )
94
+
95
+ def save(self, path: Path) -> None:
96
+ """Save build state to JSON file.
97
+
98
+ Args:
99
+ path: Path to build_state.json file
100
+ """
101
+ path.parent.mkdir(parents=True, exist_ok=True)
102
+ with open(path, "w", encoding="utf-8") as f:
103
+ json.dump(self.to_dict(), f, indent=2)
104
+
105
+ @classmethod
106
+ def load(cls, path: Path) -> Optional["BuildState"]:
107
+ """Load build state from JSON file.
108
+
109
+ Args:
110
+ path: Path to build_state.json file
111
+
112
+ Returns:
113
+ BuildState instance or None if file doesn't exist
114
+ """
115
+ if not path.exists():
116
+ return None
117
+
118
+ try:
119
+ with open(path, "r", encoding="utf-8") as f:
120
+ data = json.load(f)
121
+ return cls.from_dict(data)
122
+ except (json.JSONDecodeError, KeyError):
123
+ # Corrupted state file - return None to trigger rebuild
124
+ return None
125
+
126
+ def compare(self, other: Optional["BuildState"]) -> Tuple[bool, List[str]]:
127
+ """Compare this state with another state.
128
+
129
+ Args:
130
+ other: Previous build state (or None if no previous build)
131
+
132
+ Returns:
133
+ Tuple of (needs_rebuild, reasons)
134
+ - needs_rebuild: True if build needs to be invalidated
135
+ - reasons: List of human-readable reasons for invalidation
136
+ """
137
+ if other is None:
138
+ return True, ["No previous build state found"]
139
+
140
+ reasons = []
141
+
142
+ # Check platformio.ini changes
143
+ if self.platformio_ini_hash != other.platformio_ini_hash:
144
+ reasons.append("platformio.ini has changed")
145
+
146
+ # Check platform changes
147
+ if self.platform != other.platform:
148
+ reasons.append(f"Platform changed: {other.platform} -> {self.platform}")
149
+
150
+ # Check board changes
151
+ if self.board != other.board:
152
+ reasons.append(f"Board changed: {other.board} -> {self.board}")
153
+
154
+ # Check framework changes
155
+ if self.framework != other.framework:
156
+ reasons.append(f"Framework changed: {other.framework} -> {self.framework}")
157
+
158
+ # Check toolchain version changes
159
+ if self.toolchain_version != other.toolchain_version:
160
+ reasons.append(
161
+ f"Toolchain version changed: {other.toolchain_version} -> {self.toolchain_version}"
162
+ )
163
+
164
+ # Check framework version changes
165
+ if self.framework_version != other.framework_version:
166
+ reasons.append(
167
+ f"Framework version changed: {other.framework_version} -> {self.framework_version}"
168
+ )
169
+
170
+ # Check platform version changes
171
+ if self.platform_version != other.platform_version:
172
+ reasons.append(
173
+ f"Platform version changed: {other.platform_version} -> {self.platform_version}"
174
+ )
175
+
176
+ # Check build flags changes
177
+ if set(self.build_flags) != set(other.build_flags):
178
+ reasons.append("Build flags have changed")
179
+
180
+ # Check library dependencies changes
181
+ if set(self.lib_deps) != set(other.lib_deps):
182
+ reasons.append("Library dependencies have changed")
183
+
184
+ needs_rebuild = len(reasons) > 0
185
+ return needs_rebuild, reasons
186
+
187
+
188
+ class BuildStateTracker:
189
+ """Tracks build state for cache invalidation."""
190
+
191
+ def __init__(self, build_dir: Path):
192
+ """Initialize build state tracker.
193
+
194
+ Args:
195
+ build_dir: Build directory (.fbuild/build/{env_name})
196
+ """
197
+ self.build_dir = Path(build_dir)
198
+ self.state_file = self.build_dir / "build_state.json"
199
+
200
+ @staticmethod
201
+ def hash_file(path: Path) -> str:
202
+ """Calculate SHA256 hash of a file.
203
+
204
+ Args:
205
+ path: Path to file
206
+
207
+ Returns:
208
+ SHA256 hash as hex string
209
+ """
210
+ sha256 = hashlib.sha256()
211
+ with open(path, "rb") as f:
212
+ for chunk in iter(lambda: f.read(8192), b""):
213
+ sha256.update(chunk)
214
+ return sha256.hexdigest()
215
+
216
+ def create_state(
217
+ self,
218
+ platformio_ini_path: Path,
219
+ platform: str,
220
+ board: str,
221
+ framework: str,
222
+ toolchain_version: Optional[str] = None,
223
+ framework_version: Optional[str] = None,
224
+ platform_version: Optional[str] = None,
225
+ build_flags: Optional[List[str]] = None,
226
+ lib_deps: Optional[List[str]] = None,
227
+ ) -> BuildState:
228
+ """Create a BuildState from current configuration.
229
+
230
+ Args:
231
+ platformio_ini_path: Path to platformio.ini file
232
+ platform: Platform name
233
+ board: Board ID
234
+ framework: Framework name
235
+ toolchain_version: Toolchain version
236
+ framework_version: Framework version
237
+ platform_version: Platform version
238
+ build_flags: Build flags from platformio.ini
239
+ lib_deps: Library dependencies from platformio.ini
240
+
241
+ Returns:
242
+ BuildState instance representing current configuration
243
+ """
244
+ # Hash platformio.ini
245
+ ini_hash = self.hash_file(platformio_ini_path)
246
+
247
+ return BuildState(
248
+ platformio_ini_hash=ini_hash,
249
+ platform=platform,
250
+ board=board,
251
+ framework=framework,
252
+ toolchain_version=toolchain_version,
253
+ framework_version=framework_version,
254
+ platform_version=platform_version,
255
+ build_flags=build_flags,
256
+ lib_deps=lib_deps,
257
+ )
258
+
259
+ def load_previous_state(self) -> Optional[BuildState]:
260
+ """Load the previous build state.
261
+
262
+ Returns:
263
+ Previous BuildState or None if no previous build
264
+ """
265
+ return BuildState.load(self.state_file)
266
+
267
+ def save_state(self, state: BuildState) -> None:
268
+ """Save the current build state.
269
+
270
+ Args:
271
+ state: BuildState to save
272
+ """
273
+ state.save(self.state_file)
274
+
275
+ def check_invalidation(
276
+ self,
277
+ platformio_ini_path: Path,
278
+ platform: str,
279
+ board: str,
280
+ framework: str,
281
+ toolchain_version: Optional[str] = None,
282
+ framework_version: Optional[str] = None,
283
+ platform_version: Optional[str] = None,
284
+ build_flags: Optional[List[str]] = None,
285
+ lib_deps: Optional[List[str]] = None,
286
+ ) -> Tuple[bool, List[str], BuildState]:
287
+ """Check if build cache should be invalidated.
288
+
289
+ Args:
290
+ platformio_ini_path: Path to platformio.ini file
291
+ platform: Current platform name
292
+ board: Current board ID
293
+ framework: Current framework name
294
+ toolchain_version: Current toolchain version
295
+ framework_version: Current framework version
296
+ platform_version: Current platform version
297
+ build_flags: Current build flags
298
+ lib_deps: Current library dependencies
299
+
300
+ Returns:
301
+ Tuple of (needs_rebuild, reasons, current_state)
302
+ - needs_rebuild: True if build should be invalidated
303
+ - reasons: List of reasons for invalidation
304
+ - current_state: Current BuildState (for saving after build)
305
+ """
306
+ # Create current state
307
+ current_state = self.create_state(
308
+ platformio_ini_path=platformio_ini_path,
309
+ platform=platform,
310
+ board=board,
311
+ framework=framework,
312
+ toolchain_version=toolchain_version,
313
+ framework_version=framework_version,
314
+ platform_version=platform_version,
315
+ build_flags=build_flags,
316
+ lib_deps=lib_deps,
317
+ )
318
+
319
+ # Load previous state
320
+ previous_state = self.load_previous_state()
321
+
322
+ # Compare states
323
+ needs_rebuild, reasons = current_state.compare(previous_state)
324
+
325
+ return needs_rebuild, reasons, current_state
@@ -0,0 +1,98 @@
1
+ """Build utilities for Fbuild.
2
+
3
+ This module provides utility functions for build operations like
4
+ printing size information and formatting build output.
5
+ """
6
+
7
+ import os
8
+ import stat
9
+ import shutil
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Optional
12
+
13
+ from ..build.linker import SizeInfo
14
+
15
+
16
+ class SizeInfoPrinter:
17
+ """Utility class for printing firmware size information."""
18
+
19
+ @staticmethod
20
+ def print_size_info(size_info: Optional[SizeInfo]) -> None:
21
+ """
22
+ Print firmware size information in a formatted display.
23
+
24
+ Args:
25
+ size_info: Size information from linker (None to skip printing)
26
+ """
27
+ if not size_info:
28
+ return
29
+
30
+ print("Firmware Size:")
31
+ print(f" Program: {size_info.total_flash:6d} bytes", end="")
32
+ if size_info.flash_percent is not None:
33
+ print(
34
+ f" ({size_info.flash_percent:5.1f}% of {size_info.max_flash} bytes)"
35
+ )
36
+ else:
37
+ print()
38
+
39
+ print(f" Data: {size_info.data:6d} bytes")
40
+ print(f" BSS: {size_info.bss:6d} bytes")
41
+ print(f" RAM: {size_info.total_ram:6d} bytes", end="")
42
+ if size_info.ram_percent is not None:
43
+ print(f" ({size_info.ram_percent:5.1f}% of {size_info.max_ram} bytes)")
44
+ else:
45
+ print()
46
+
47
+
48
+ def remove_readonly(func: Callable[[str], None], path: str, excinfo: Any) -> None:
49
+ """
50
+ Error handler for shutil.rmtree on Windows.
51
+
52
+ On Windows, read-only files cannot be deleted and will cause
53
+ shutil.rmtree to fail. This handler removes the read-only attribute
54
+ and retries the operation.
55
+
56
+ Args:
57
+ func: The function that raised the exception
58
+ path: The path to the file/directory
59
+ excinfo: Exception information (unused)
60
+ """
61
+ os.chmod(path, stat.S_IWRITE)
62
+ func(path)
63
+
64
+
65
+ def safe_rmtree(path: Path, max_retries: int = 3) -> None:
66
+ """
67
+ Safely remove a directory tree, handling Windows-specific issues.
68
+
69
+ This function handles common Windows issues with directory deletion:
70
+ - Read-only files
71
+ - Locked files (with retries)
72
+ - Hidden system files
73
+
74
+ Args:
75
+ path: Path to directory to remove
76
+ max_retries: Maximum number of retry attempts for locked files
77
+
78
+ Raises:
79
+ OSError: If directory cannot be removed after all retries
80
+ """
81
+ import time
82
+
83
+ if not path.exists():
84
+ return
85
+
86
+ for attempt in range(max_retries):
87
+ try:
88
+ shutil.rmtree(path, onerror=remove_readonly)
89
+ return
90
+ except OSError as e:
91
+ if attempt < max_retries - 1:
92
+ # Wait a bit and retry (files might be temporarily locked)
93
+ time.sleep(0.5)
94
+ else:
95
+ # Last attempt failed, raise the error
96
+ raise OSError(
97
+ f"Failed to remove directory {path} after {max_retries} attempts: {e}"
98
+ ) from e